Lesson 0004 · Caching patterns
The pattern behind almost every Redis cache — and the thundering herd that TTLs quietly set up.
You now know (Lesson 0003) that a TTL makes a key vanish at a moment in time. This lesson builds the caching pattern that relies on that — cache-aside — and then confronts the failure it sets up: the instant a hot key expires, thousands of requests miss at once and stampede your database. Getting cache-aside right is table stakes; knowing how to tame the stampede is what separates "I use Redis" from "I run Redis in production."
Cache-aside puts the application in charge: it consults the cache, and only touches the database on a miss. The read path is three steps — AWS — Database Caching Strategies (Caching patterns):
# cache-aside read
val = GET key
if val is nil: # cache miss
val = db.query(...) # go to the source of truth
SET key val EX 300 # populate + TTL (lazy load)
return val
Because the cache fills only on demand, it holds just what the app actually
requests — cheap and self-trimming. The cost: every miss pays one slow DB round-trip, and the
data can go stale between the DB changing and the TTL expiring. On writes,
cache-aside is usually paired with either write-through (update DB, then
immediately update the cache) or simply invalidation (DEL key on
write, let the next read repopulate).
— AWS whitepaper
| Pattern | Who loads the cache | When |
|---|---|---|
| Cache-aside (lazy) | Application code | On a read miss |
| Read-through | The cache layer itself | On a read miss (transparently) |
| Write-through | Application/backend | Synchronously on every DB write |
| Write-behind (write-back) | Cache | Write to cache now, flush to DB later (async) |
AWS documents cache-aside and write-through in detail; read-through and write-behind are the standard industry terms for the other two rows.
A cache stampede (a.k.a. thundering herd or dogpile) is a cascading failure: a popular key expires, and every concurrent request that was being served from it now misses simultaneously — so they all run step 2 (query the DB) at the same instant. The database, which the cache existed to protect, takes the full unbuffered load. — Cache stampede (Vattani et al., VLDB 2015)
It compounds. The recomputation is slow (that's why you cached it), so the herd keeps arriving during the recompute window — hundreds of requests each kicking off their own identical DB query. And if your TTLs were all set together (e.g. a bulk cache warm), keys expire in lockstep and many hot keys stampede at the same second.
Add a random spread to every TTL so keys don't expire in lockstep:
EX (300 + rand(0..60)). Cheapest possible fix, kills the synchronized-expiry
variant outright.
— AWS ElastiCache — add jitter to TTLs
On a miss, have exactly one request rebuild the value while the rest wait briefly or
serve stale. Redis gives you the atomic primitive for free (recall from
Lesson 0001: commands are atomic):
SET lock:key token NX EX 10 succeeds for only one caller.
— Redis Docs — SET NX EX
val = GET key
if val is nil:
if SET lock:key <token> NX EX 10: # exactly one winner
val = db.query(...)
SET key val EX (300 + jitter)
# release only our own lock (see warning)
else:
sleep a little / serve stale, then re-GET
return val
Releasing the lock with a bare DEL lock:key is unsafe: if your work outran the
lock's TTL, you'd delete a different client's lock. Store a random token and release
only if it still matches, via a Lua script (atomic check-and-delete). For locking across
multiple Redis nodes, the single-instance lock has a real failure mode (async replication can
hand two clients the same lock) — that's what the Redlock algorithm exists to
address.
— Redis Docs — Distributed Locks
Instead of waiting for expiry, let each reader probabilistically refresh the key
before it dies, with the probability rising as the TTL approaches. One lucky request
rebuilds early; everyone else keeps getting a cache hit — the herd never forms. This is the
Vattani et al. result; the trigger recomputes when
now − delta·β·ln(rand) ≥ expiry, where delta is the last recompute
duration and β tunes eagerness.
— "Optimal Probabilistic Cache Stampede Prevention", VLDB 2015
Serve the last-known value while one background refresh runs — no reader ever blocks on the DB. Keep the value under a longer "hard" TTL than its "fresh" window; past freshness, return stale and trigger a single async rebuild. This is the same idea RFC 5861 standardized for HTTP caches. — RFC 5861 — stale-while-revalidate
Jitter is free — do it always. Add a single-flight lock for expensive-to-compute hot keys. Reach for XFetch or stale-while-revalidate when even a brief per-key stall is unacceptable and you can tolerate serving slightly stale data. They compose: jitter + lock covers most real systems.
Answer from memory — effortful recall builds retention. Two questions revisit earlier lessons on purpose (spacing).
In cache-aside, the database is read:
A cache stampede happens the moment a hot key:
Which Redis command atomically elects one request to rebuild a key?
The cheapest fix for many keys expiring in lockstep is to:
Interleave — overwriting a cache key with a bare SET (no KEEPTTL) does what to its TTL?
Interleave — your Redis cache has no TTLs and fills maxmemory. Correct policy?
You need a local Redis and redis-cli. We'll fake a "slow DB" with a sleep.
# two terminals racing to rebuild "report" — only one should win the lock
redis-cli SET lock:report t1 NX EX 10 # terminal A -> OK
redis-cli SET lock:report t2 NX EX 10 # terminal B -> (nil): blocked
# A does the slow work, writes the value with jitter, then releases safely:
redis-cli SET report "…computed…" EX 305
redis-cli EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:report t1
redis-cli EVAL "for i=1,5 do redis.call('set','p:'..i,i,'EX',60+math.random(0,30)) end" 0
redis-cli TTL p:1
redis-cli TTL p:2 # different — no lockstep expiry
In step 1, why does terminal B get (nil)? And what would break if A released the
lock with a bare DEL lock:report after its work ran past 10 seconds? Commit, then
reason it through with the warning above.
Clean up with redis-cli FLUSHALL. Bring your lock-race reasoning to your teacher.
AWS — Database Caching Strategies Using Redis: Caching patterns is the clearest authoritative write-up of cache-aside vs write-through. For the stampede, skim Cache stampede and, if you want the theory, the Vattani VLDB paper.