Cache Stampede
A cache stampede occurs when many requests try to rebuild the same cached value at once. It commonly happens when a popular key expires, is evicted, or is invalidated. It is most visible on high-traffic keys with short TTLs.
sequenceDiagram
actor Clients
participant App as Application Instances
participant Cache
participant DB as Data Source
Note over Cache: Popular key expires
Clients->>App: Many concurrent requests
App->>Cache: Read the same key
Cache-->>App: Cache miss
App->>DB: Rebuild value
App->>DB: Rebuild value
App->>DB: Rebuild value
Note over DB: Sudden duplicate load
DB-->>App: Return values
App->>Cache: Store the same value repeatedly
App-->>Clients: Return responses
Prevention
Locking / Request coalescing / Single-flight
Allow only one request to compute a missing value. Other requests wait for that computation and reuse its result.
Example code:
value = cache.get(key)
if value exists:
return value
if acquire_lock(key, short_timeout):
try:
value = cache.get(key) // check again after acquiring the lock
if value does not exist:
value = load_from_source()
cache.set(key, value, ttl)
return value
finally:
release_lock(key)
else:
wait briefly, then retry cache.get(key)
Use when:
- the key is very expensive to rebuild
- a small number of hot keys gets hammered by many requests
- need to prevent duplicate recomputation as much as possible
Best for:
- user profiles
- product pages
- expensive DB aggregates
- any cache miss that would overload the backend
Tradeoff:
- adds waiting/latency for concurrent requests
Note:
the lock should have a short TTL/timeout so a crashed worker does not block refresh forever.
Stale-While-Revalidate
The system immediately serves old cached data to users while a background worker asynchronously recomputes fresh data.
sequenceDiagram
actor Client
participant App
participant Cache
participant Source as Data Source
Client->>App: Request data
App->>Cache: Read value
Cache-->>App: Stale but within grace period
App-->>Client: Return stale value immediately
App->>Source: Refresh in background
Source-->>App: Return current value
App->>Cache: Replace cached value
Use when:
- serving a slightly old value is acceptable
- need fast responses even during refresh
- freshness matters, but not every request needs the newest data
Best for:
- dashboards
- feeds
- metrics
- content pages where brief staleness is fine
Tradeoff:
- users may see stale data for a short period
Refresh ahead
Use when:
- can predict popular keys
- want to refresh before users hit expiration
- have a background system already running
Best for:
- highly predictable hot keys
- scheduled reports
- frequently accessed reference data
Tradeoff:
- may refresh unused keys and waste resources
Add TTL jitter
Add a small random offset to each expiration time so that related keys do not expire simultaneously.
effective_ttl = base_ttl + random(-jitter, +jitter)
TTL jitter spreads load over time, but it does not prevent a stampede on one extremely popular key. Combine it with request coalescing or stale-while-revalidate.
Use when:
- many related keys expire at the same time
- need to avoid synchronized expiration bursts
Best for:
- bulk-generated cache entries
- large sets of similar objects
- systems where expiration alignment is causing spikes
Tradeoff:
- helps spread load, but doesn’t fully solve a single hot-key stampede
Probabilistic Early Expiration
Probabilistic early expiration refreshes a key before its TTL fully ends, but only for some requests and with a random chance.
The chance of early refresh usually increases as the key gets closer to expiry.
sequenceDiagram
actor Client
participant App
participant Cache
participant Source as Data Source
Client->>App: Request key
App->>Cache: Read cached value
Cache-->>App: Value + remaining TTL
App->>App: Check age / expiry threshold
alt Not near expiry
App-->>Client: Return cached value
else Near expiry
App->>App: Compute early-refresh probability
alt Keep cached value
App-->>Client: Return cached value
else Refresh early
App->>Source: Recompute / fetch fresh value
Source-->>App: Fresh value
App->>Cache: Store fresh value with new TTL
App-->>Client: Return fresh value
end
end
Flow summary:
- The app reads the cached value and checks how close it is to expiry.
- If it is not near expiry, the cached value is returned.
- If it is near expiry, the app computes a probability of early refresh.
- Some requests still return the cached value.
- Some requests trigger an early refresh and write a new value back to the cache.
- This spreads refreshes over time and reduces stampede risk.
Use when:
- need to smooth refresh load
- can tolerate occasional early recomputation
- need a lighter-weight alternative to strict locking
Best for:
- large-scale caching systems
- traffic patterns where many requests race near expiry
- distributed systems where exact coordination is costly
Tradeoff:
- duplicate refreshes can still happen
External recomputation
Move the recomputation logic to an external background process that updates the cache periodically or before expiration. This is best for data that can be precomputed or periodically refreshed.
Use when:
- recomputation is heavy or batchable
- freshness can be managed by a separate worker
- need to keep request path simple
Best for:
- nightly aggregations
- precomputed leaderboards
- derived analytics data
Tradeoff:
- more infrastructure and less on-demand freshness
Practical rule of thumb
- Need strong protection against duplicate work? → Locking / single-flight
- Can serve slightly stale data? → Stale-While-Revalidate
- Have predictable hot keys? → Refresh ahead
- Many keys expire together? → TTL jitter
- Want to smooth traffic without strict locking? → Probabilistic early expiration
- Recompute can happen outside requests? → External recomputation
It is often safer to combine two protections, such as locking + stale-while-revalidate.