Preventing Cache Stampede with Redis
A cache stampede is one of those failures that stays hidden until traffic is high, then takes down your database at the worst possible moment. A popular cached key expires, and in the brief instant before it's repopulated, hundreds of concurrent requests all miss and all hit the database at once to rebuild the same value. The cache, which existed to protect the database, momentarily stops protecting it precisely when load is highest. This article covers why it happens and the three techniques that prevent it.
This is part 14 of the Redis Masterclass, following cache invalidation.
Anatomy of a stampede
Picture a key caching an expensive query, serving 500 requests per second, with a 10-minute TTL. For 10 minutes everything is fast, every request a cache hit. Then the key expires. In the next few milliseconds, before any single request finishes rebuilding it, dozens or hundreds of concurrent requests all find the key missing. Each one independently runs the expensive query against the database. The database, which was handling near-zero load for this key, suddenly gets slammed with hundreds of identical expensive queries.
The result is a load spike that can slow or crash the database, which slows the rebuilds, which lets even more requests pile up. This is also called the thundering herd or dog-piling. The hotter the key, the worse the stampede, which is exactly backwards from what you want.
Technique 1: locking (request coalescing)
The core fix is to ensure only one request rebuilds the key while the others wait for the result. On a miss, a request tries to acquire a short lock (the SET NX from the strings article). Whoever gets the lock rebuilds the value; everyone else waits briefly and reads the freshly-cached result:
async function getWithLock(key) {
let value = await redis.get(key);
if (value) return value; // hit
const gotLock = await redis.set(`lock:${key}`, '1', 'NX', 'EX', 10);
if (gotLock) {
value = await loadFromDb(); // only one request does this
await redis.set(key, value, 'EX', 600);
await redis.del(`lock:${key}`);
return value;
} else {
await sleep(50); // someone else is rebuilding
return getWithLock(key); // retry, likely a hit now
}
}
Now a stampede of 500 misses results in one database query, not 500. The lock coalesces the herd into a single rebuild. This is the most direct and widely-used defense.
Technique 2: TTL jitter
A subtler cause of stampedes is many keys expiring at the same moment. If you cache a batch of items all with the same TTL at the same time (say, warming the cache on deploy), they all expire together, and you get a synchronized stampede across many keys at once.
The fix is jitter: add a small random amount to each TTL so expirations spread out instead of clustering:
const ttl = 600 + Math.floor(Math.random() * 60); // 600-660s, not all 600
await redis.set(key, value, 'EX', ttl);
Now the keys expire staggered across a 60-second window rather than all at once, so rebuilds spread out and never form a single crushing spike. Jitter is cheap and worth applying by default to any batch of keys cached together. It also helps with the single-key case by desynchronizing repeated rebuild cycles.
Technique 3: early recomputation (probabilistic)
The most elegant technique refreshes a key before it expires, probabilistically, so it's rebuilt by one request while still serving the cached value to everyone else, and the key never actually expires under load. This is refresh-ahead's idea applied to stampede prevention. A known approach recomputes with a probability that rises as expiry approaches:
# on each read, with small increasing probability as the key nears expiry,
# one request rebuilds it early in the background while others keep hitting the warm cache
Because the recompute happens before expiry, there's never a moment when the key is missing and the herd can form. Only one request (the one that "won" the probabilistic check) does the rebuild, and it does so without any request having to wait. This is more complex to implement than locking, but it eliminates the miss window entirely rather than just coalescing the herd after a miss.
Combining the defenses
These aren't mutually exclusive, and a well-protected cache uses more than one:
- Locking handles the general case: whenever a key misses, only one request rebuilds it. This is the baseline defense to apply broadly.
- TTL jitter prevents synchronized expiry, and it's so cheap you should apply it to essentially all cached data.
- Early recomputation is worth adding for your hottest, most expensive keys, where even a coalesced rebuild spike is worth avoiding entirely.
A practical setup: jitter on all TTLs by default, locking on the read path so misses coalesce, and early recomputation on the handful of critical hot keys. Together they mean a hot key expiring never turns into a database-crushing herd.
Cache stampede is the failure where your cache abandons the database exactly when it's needed most. The fixes are well understood: coalesce concurrent rebuilds with a lock, stagger expirations with jitter, and for the hottest keys, refresh before expiry so the miss never happens. Apply jitter everywhere and locking on the read path, and the stampede that quietly waits for your next traffic spike simply doesn't fire.
That completes the core caching patterns and their failure modes. Next, we close the caching module with a practical roundup of best practices and the pitfalls that catch teams in production.
Key takeaways
- A cache stampede happens when a hot key expires and many concurrent requests all miss and hit the database at once.
- Locking (via `SET NX`) lets only one request rebuild a missed key while others wait, coalescing the herd into one query.
- TTL jitter adds randomness to expiry times so keys cached together don't all expire simultaneously; apply it by default.
- Early (probabilistic) recomputation refreshes hot keys before they expire, eliminating the miss window entirely.
- Combine all three: jitter everywhere, locking on the read path, and early recomputation for the hottest keys.
Frequently asked questions
What is a cache stampede?
When a popular cached key expires and many concurrent requests all miss simultaneously, each independently querying the database to rebuild the same value. The resulting load spike can overwhelm the database exactly when the key is hottest. It's also called the thundering herd.
How do I prevent a cache stampede?
Use a lock so only one request rebuilds a missed key while others wait (request coalescing), add jitter to TTLs so keys don't expire in sync, and for the hottest keys, recompute them before they expire so the miss never occurs. Combining these covers all cases.
How does locking stop a stampede?
On a cache miss, requests try to acquire a short `SET NX` lock. Only the one that gets the lock rebuilds the value and repopulates the cache; the others wait briefly and then read the fresh result. A herd of misses becomes a single database query.
What is TTL jitter and why does it help?
Adding a small random amount to each key's TTL so keys cached at the same time don't all expire at the same moment. It spreads rebuilds across a window instead of clustering them into one synchronized spike.
What is early recomputation?
Refreshing a hot key before it expires, typically with a probability that increases as expiry nears, so one request rebuilds it in the background while others keep hitting the still-valid cache. The key never reaches an expired state, so no stampede can form.
Explore more on

About the author
I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.