Problem
When a popular key expires, every concurrent request misses simultaneously and queries the database for the same value. The burst can overload the database, slowing regeneration further and deepening the outage.
Also known as: thundering-herd-cache, dogpile-prevention
Prevent many concurrent requests from regenerating the same expired cache entry at once.
When a popular key expires, every concurrent request misses simultaneously and queries the database for the same value. The burst can overload the database, slowing regeneration further and deepening the outage.
A cache stampede, or dogpile, happens when many requests find the same key missing and all rebuild it. The cache existed to protect the database, and at the moment of expiry it does the opposite.
get(key):
entry = cache.get(key)
if entry and now < entry.softExpiry: return entry.value
if cache.setnx("lock:" + key, 1, ttl=10):
value = loader(key) # only one caller
cache.set(key, wrap(value, soft=now+60), ttl=120)
cache.delete("lock:" + key)
return value
if entry: return entry.value # serve stale
wait briefly, then retry get(key)
The entry carries a soft expiry and a longer hard TTL. Between them, readers get the stale value while exactly one caller refreshes. Without stale data to serve, losing callers wait or poll the cache for a short time.
Probabilistic early expiration needs no lock: each reader recomputes when now minus (delta times beta times the log of a random number) passes expiry, where delta is the last compute time. Slow computations start refreshing earlier.
Negligible; small extra cache keys for locks
Low: monitor lock contention
Medium: concurrency edge cases
Lock holder crashes and waiters block until lock timeout
Background refresh fails repeatedly and stale data is served indefinitely
Jitter too narrow, so expirations still cluster