Three Redis failures that look similar until production
Cache penetration, stampede and avalanche all look like "Redis fell over," but each has a different cause and a different fix. Here's how to tell them apart.

Every team that adds Redis to cut database load eventually gets paged by Redis. The confusing part is that three genuinely different failure modes all present the same symptom, origin database suddenly on fire, and the fix for one does nothing for the other two. Here’s how to tell them apart.
Cache penetration: the cache was never going to help
Penetration happens when requests ask for keys that don’t exist, so there’s nothing to cache in the first place. Every request is a guaranteed miss, which means every request skips the cache entirely and lands on the origin. A cache can’t protect you from a lookup it was never able to satisfy.
This usually isn’t random bad luck, it’s often adversarial (someone probing your API with IDs that don’t exist) or it’s a bug (a client requesting a record that was just deleted, in a retry loop). Either way, Redis sits there doing nothing useful while your database absorbs full traffic.
Fixes that actually address this:
- Negative caching, cache the fact that a key doesn’t exist, with a short TTL. A second lookup for the same missing key now hits Redis instead of the database.
- Bloom filters, a probabilistic structure in front of the cache that can cheaply answer “this key definitely doesn’t exist” without touching Redis or the database at all. Good when the space of invalid keys is large and adversarial.
Cache stampede: one key, everyone wants it right now
A stampede happens when a single hot key expires (or was never cached) and a burst of concurrent requests for that exact key all miss at once. Each of those requests independently decides “cache miss, go recompute this from the origin”, so instead of one request hitting the database, you get hundreds, all doing the same expensive work, all at the same moment.
This is the classic “popular product page” or “trending post” problem: one key, many readers, synchronized by the bad luck of hitting the same TTL boundary.
Fixes that actually address this:
- Request coalescing, the first request to miss takes responsibility for recomputing the value; concurrent requests for the same key wait on that one computation instead of starting their own.
- Locking, similar idea, implemented as a short-lived lock on the key while it’s being recomputed.
- Stale-while-revalidate, serve the expired value immediately while one background request refreshes it, so nobody waits and the origin only sees one extra request, not a burst.
Cache avalanche: everything expires together
An avalanche is the stampede’s bigger, meaner sibling: instead of one hot key expiring at a bad moment, many keys expire around the same time, usually because they were all written (and given the same TTL) in the same batch job or deploy. When that TTL window closes, a large fraction of your cache goes cold simultaneously, and the origin gets hit with the combined weight of requests that used to be spread across a warm cache.
Fixes that actually address this:
- TTL jitter, add a small random offset to each key’s expiration instead of using one flat TTL for a whole batch, so expirations spread out over time instead of landing on the same second.
- Staggered cache warming, if you’re pre-populating the cache in a batch
job, don’t write every key with the same TTL computed from the same
now().
A quick way to tell them apart
| Failure | Trigger | Signature | Primary fix |
|---|---|---|---|
| Penetration | Requests for keys that don’t exist | Cache never helps, every request is a guaranteed miss | Negative caching, Bloom filter |
| Stampede | One hot key expires under concurrent load | Sudden burst on a single key | Coalescing, locking, stale-while-revalidate |
| Avalanche | Many keys expire together | Broad, simultaneous cold cache | TTL jitter, staggered warming |
The failure point moves, it doesn’t disappear
The useful mental model here: adding Redis doesn’t remove the possibility of your database getting overwhelmed, it relocates where that failure can happen and changes its shape. Before Redis, every request hits the database predictable, if slow. After Redis, most requests are fast, but the specific conditions under which the cache stops helping (penetration, stampede, avalanche) become new, sharper failure modes that are easy to miss in testing because your test traffic rarely reproduces “many keys expiring at once” or “a burst of concurrent requests for one key.”
If you’re adding a cache layer, write a test that specifically produces a cache miss under load, not just a test that the cache works when warm. That’s the scenario that actually matters in production, and it’s the one most test suites skip.