What is a cache stampede (thundering herd) and how do you prevent it in Redis?
Learn what a cache stampede (thundering herd) is in Redis and how to prevent it with locks, TTL jitter, early expiration, and stale-while-revalidate.
Expected Interview Answer
A cache stampede (thundering herd) happens when a hot cache key expires and many concurrent requests miss simultaneously, all rushing to recompute the same value and hammering the backing database at once. You prevent it by serializing the recompute with a lock, staggering expirations, or refreshing values before they expire.
The classic fix is a mutex: the first request that sees the miss acquires a short-lived Redis lock (SET key value NX PX) and recomputes, while other requests briefly wait or serve a stale value. Complementary techniques include probabilistic early expiration (XFetch), where a value is refreshed slightly before its TTL based on recompute cost, adding random jitter to TTLs so keys do not all expire together, and background refresh so the cache is never actually empty. Serving stale-while-revalidate data keeps latency low during the refresh window.
- Protects the origin database from sudden load spikes
- Keeps tail latency stable when hot keys expire
- Avoids redundant duplicate recomputation of the same value
- Allows graceful stale-while-revalidate responses
- Scales predictably under high concurrency
AI Mentor Explanation
Picture a stadium with one water tap and thousands of fans who all get thirsty at the exact drinks break. If everyone charges the tap at once it jams and no one drinks. Instead, one steward fills a jug while others wait, then shares it around. A cache lock works the same way: the first request refills the value while the rest wait for the jug rather than all mobbing the source.
Step-by-Step Explanation
Step 1
Detect the miss
A request finds the hot key absent or expired and would normally recompute from the database.
Step 2
Acquire a lock
Attempt SET lock:key token NX PX 3000 so only one request wins the right to recompute.
Step 3
Recompute or wait
The lock holder queries the origin and repopulates the cache; other requests wait briefly or serve stale data.
Step 4
Repopulate with jittered TTL
Store the fresh value with a randomized TTL so many keys never expire in the same instant.
Step 5
Release and serve
Release the lock, and all waiting requests read the freshly cached value instead of hitting the database.
What Interviewer Expects
- Clear description of why simultaneous misses overwhelm the origin
- Knowledge of the SET NX PX mutex approach
- Awareness of TTL jitter and early/probabilistic expiration
- Understanding of stale-while-revalidate and background refresh
- Trade-offs between waiting, serving stale, and locking
Common Mistakes
- Confusing a cache stampede with a simple cache miss
- Using a lock without a TTL, risking a permanent deadlock
- Setting identical TTLs on all hot keys so they expire together
- Blocking all requests instead of serving stale data during refresh
- Ignoring lock-holder crashes and never releasing the lock
Best Answer (HR Friendly)
“A cache stampede is when a popular cached item expires and lots of users hit the slow database at the same time to rebuild it, which can overload the system. You prevent it by letting just one request rebuild the value while others wait or use the old copy, and by making items expire at slightly different times.”
Code Example
async function getWithLock(redis, key, ttlMs, recompute) {
const cached = await redis.get(key);
if (cached !== null) return JSON.parse(cached);
const lockKey = `lock:${key}`;
const token = crypto.randomUUID();
// Only one request wins the lock (NX), auto-expires (PX) to avoid deadlock
const gotLock = await redis.set(lockKey, token, 'NX', 'PX', 3000);
if (!gotLock) {
// Someone else is rebuilding: wait briefly then read the fresh value
await new Promise((r) => setTimeout(r, 50));
return getWithLock(redis, key, ttlMs, recompute);
}
try {
const value = await recompute();
const jitter = Math.floor(Math.random() * 0.2 * ttlMs); // spread expirations
await redis.set(key, JSON.stringify(value), 'PX', ttlMs + jitter);
return value;
} finally {
// Release only if we still own the lock
const lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
await redis.eval(lua, 1, lockKey, token);
}
}Follow-up Questions
- How does probabilistic early expiration (XFetch) decide when to refresh?
- What happens if the lock holder crashes mid-recompute?
- How would you serve stale-while-revalidate data safely?
- Why is adding a TTL to the lock itself essential?
- How does this interact with a multi-node Redis cluster?
MCQ Practice
1. What primarily triggers a cache stampede?
A stampede occurs when a popular key expires and many requests simultaneously miss and recompute it, flooding the origin.
2. Which Redis command best implements a single-recompute mutex?
SET with NX sets the lock only if absent, and PX gives it an auto-expiry so a crashed holder cannot deadlock the key.
3. Why add random jitter to cache TTLs?
Jitter staggers expiration times, preventing a synchronized wave of misses that would otherwise cause a stampede.
Flash Cards
What is a cache stampede? — Many concurrent requests missing the same expired hot key and all recomputing it, overwhelming the origin database.
Primary lock command to prevent it? — SET lock token NX PX <ttl> — one winner recomputes, and the PX expiry avoids deadlock.
What does TTL jitter do? — Adds randomness to expiry times so hot keys do not all expire simultaneously.
What is stale-while-revalidate? — Serving the old cached value to most requests while one request refreshes it in the background.
Risk of a lock without a TTL? — If the holder crashes, the lock is never released and the key deadlocks permanently.
Continue Learning
Related Interview Questions
How does Redis client-side caching with CLIENT TRACKING work, and when is it worth it?
hard
What is Redis and what makes it different from a traditional relational database?
easy
What is the difference between write-through, write-behind, and cache-aside patterns?
medium
How do you implement a distributed lock with Redis?
hard