Redis — Caching Patterns, TTL & Eviction Policies
Cache-aside, stampede prevention, and avalanche prevention aren't Redis-specific ideas — this chapter is where the general patterns from NoSQL Foundations get wired up with real EXPIRE/SET/maxmemory commands.
Learning objectives
- Set and inspect TTLs on Redis keys using EXPIRE, TTL, and SET ... EX.
- Choose the correct maxmemory-policy for a given workload.
- Implement cache-aside with Redis, including a stampede-prevention lock.
- Recognize and prevent a cache avalanche caused by identical TTLs across many keys.
A cache key that never expires isn't a cache — it's a slowly-growing memory leak with a Redis interface. EXPIRE key seconds (or PEXPIRE key milliseconds) attaches a time-to-live to an existing key, after which Redis removes it automatically. TTL key reports the remaining seconds, -1 if the key exists but has no TTL set, and -2 if the key doesn't exist at all. PERSIST key removes a TTL, making the key permanent again.
In practice, SET key value EX seconds — setting the value and its TTL as one atomic command — is almost always preferable to calling SET and then EXPIRE as two separate calls, because the two-call version has a real, if narrow, window where the key exists with no TTL at all (if the process crashes or another client reads the key between the two calls).
▲ Common mistake
Calling plain SET key newvalue to update a key that already had a TTL, and being surprised the TTL is gone. SET without KEEPTTL replaces the key entirely, including its expiration — the previous TTL doesn't carry over to the new value. Use SET key newvalue KEEPTTL if you want to update the value without resetting or clearing the expiration.
💻 Code example
SET session:abc123 "user:42" EX 1800 -- 30-minute session TTL, set atomically TTL session:abc123 -- seconds remaining, or -1 / -2 EXPIRE session:abc123 3600 -- extend an existing key's TTL PERSIST session:abc123 -- remove the TTL entirely SET session:abc123 "user:42-updated" KEEPTTL -- update value, keep existing TTL
maxmemory caps how much RAM a Redis instance is allowed to use. Once the instance hits that cap, every new write needs somewhere to fit, and maxmemory-policy decides what gets evicted to make room — or whether writes are refused instead.
| Policy | Evicts | Best for |
|---|---|---|
noeviction | Nothing — new writes error out once full | An instance holding primary data (Chapter 06's @RedisHash) where silently losing a key is worse than a write failing |
allkeys-lru | Least-recently-used key, from the entire keyspace | A pure cache where every key is equally disposable |
volatile-lru | Least-recently-used key, but only among keys that have a TTL set | A mixed instance — cache keys (with TTLs) are evictable, keys with no TTL are protected |
allkeys-lfu | Least-frequently-used key, from the entire keyspace | A pure cache where access frequency predicts future value better than recency |
volatile-lfu | Least-frequently-used, TTL-bearing keys only | Mixed instance, frequency-based |
volatile-ttl | The key with the nearest expiry first | Biasing eviction toward keys that were going to disappear soon anyway |
The volatile-* policies only ever touch keys that have a TTL — which is exactly why setting a TTL on every genuinely-cache key (§4.1) matters beyond just freeing memory eventually: on a mixed instance running volatile-lru, a key with no TTL is effectively marked "this isn't disposable," and Redis will never evict it to make room, no matter how much memory pressure there is.
▲ Common mistake
Running allkeys-lru (or any allkeys-* policy) on an instance that also holds some non-cache data with no other copy anywhere. Under memory pressure, Redis will evict that data exactly as readily as a disposable cache entry — allkeys-* policies make no distinction at all between "this is a cache" and "this is the only copy."
💻 Code example
# redis.conf maxmemory 2gb maxmemory-policy volatile-lru
NoSQL Foundations, Chapter 3 §3.1 covers cache-aside as the default caching pattern in the abstract — application code checks the cache, and on a miss, reads the source of truth and populates the cache itself. Applied concretely with Redis, that's three commands:
GET product:101— check the cache.- On a miss (
nilreturned): query Postgres (or whatever the source of truth is) for product 101. SET product:101 <value> EX 300— populate the cache with a TTL, so it self-heals if the underlying row changes and nobody remembers to evict it explicitly.
The TTL is what keeps cache-aside honest without extra invalidation logic: even if a write to Postgres never explicitly evicts the cached copy, the cache entry ages out on its own and the next read repopulates it from the current source-of-truth value. This is also exactly why Chapter 02 §2.5 noted that pure-cache Redis instances can safely run with persistence disabled — every key here is reproducible from Postgres on demand, on a cache miss, by design.
💻 Code example
GET product:101 -- (nil) → cache miss -- application queries Postgres for product 101, gets back the row SET product:101 "{\"price\":4999,\"stock\":42}" EX 300
◆ The problem
product:101's TTL expires. In the next few milliseconds, five hundred concurrent requests for that same product all check the cache, all miss at the same instant, and all independently go query Postgres to refill it — the exact cache stampede NoSQL Foundations, Chapter 3 §3.2 names, now happening concretely on this one hot key.
The standard fix: the first request to miss acquires a short-lived lock key before querying the source of truth, using SET lock:product:101 <token> NX EX 5 — NX means "only set if the key doesn't already exist," so this single atomic command is simultaneously the lock-acquisition attempt and its own expiry safety net (if the holder crashes before releasing it, the lock disappears on its own in 5 seconds rather than deadlocking every future request). Everyone else who misses the cache in that window sees the lock already held, and either waits briefly and retries the cache read (the lock-holder will have repopulated it by then) or serves slightly-stale data instead of also hammering Postgres.
▲ Common mistake
Releasing the lock with a plain DEL lock:product:101 once the cache is repopulated. If the lock already expired on its own (the holder was slower than the 5-second TTL) and a different request has since acquired it legitimately, this DEL deletes someone else's active lock — exactly the accidental-unlock bug Chapter 05's Lua-scripting example solves properly, by checking the lock's token matches before deleting, as one atomic script instead of two separate commands.
💻 Code example
-- first request to miss the cache: SET lock:product:101 "req-8f3a" NX EX 5 -- "OK" → lock acquired, safe to query Postgres and refill the cache -- (nil) → someone else already holds it; wait briefly and retry the cache read instead
A batch job caches a thousand product listings at once, all with EX 300 — five minutes. Five minutes later, all thousand keys expire in the same instant, and the next wave of requests for any of them all miss simultaneously — the cache avalanche NoSQL Foundations, Chapter 3 §3.2 describes, caused here not by one hot key but by many keys sharing one exact expiry moment.
The fix is the same one NoSQL Foundations names generically, applied directly: add a small random jitter to each TTL so the batch's expirations spread out over time instead of landing on the same second. EX 300 + random(0, 60) turns one guaranteed pileup at the five-minute mark into expirations smeared across a full extra minute — trivial to add, and worth doing by default any time a cache-warming step sets the same base TTL across many keys at once rather than one key at a time.
💻 Code example
-- instead of caching every key in a batch with the same fixed TTL: SET product:101 "..." EX 300 SET product:102 "..." EX 300 -- (repeated for hundreds of keys — all expire at the same instant) -- add jitter so expirations spread out: SET product:101 "..." EX 327 -- 300 + random(0, 60) SET product:102 "..." EX 341 -- 300 + random(0, 60)
Want a visual for this concept?
Generate a diagram tailored to “Redis — Caching Patterns, TTL & Eviction Policies” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →