# How do I prevent a Redis cache stampede (thundering herd) — mutex lock or XFetch?

> A single-flight lock plus TTL jitter covers most cases; reach for XFetch only when recompute is truly expensive, and add stale-while-revalidate either way.

- Asked: 2026-05-16
- Answered: 2026-05-19
- Asked by: Cem
- Tags: performans, redis, caching
- Source: https://www.muhammetsafak.com.tr/en/just-ask/preventing-cache-stampede-with-mutex-locks-or-early-expiration/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** We cache a high-traffic e-commerce homepage's product list in Redis for 10 minutes. The instant the cache expires, thousands of concurrent requests get a cache miss and all hammer PostgreSQL at once, taking the DB down.

How do I solve this cache stampede at the architecture level with a mutex lock or probabilistic early expiration (XFetch)?


Short answer: the problem isn't the "miss" itself, it's that the key **falls off a cliff at the same instant** and the whole herd hammers the DB at once. The fix is to reduce that moment to a single request and spread the herd over time.

What you're hitting is the classic **thundering herd**: when the TTL expires, not one user but thousands get a cache miss in the same millisecond and fire the same query at PostgreSQL. Hundreds run when one would do, and the DB buckles.

1. **Single-flight mutex lock.** On a miss, the **first** request takes a short Redis lock (`SET NX PX`, with a millisecond TTL). Whoever holds the lock recomputes and fills the cache; everyone else either briefly serves stale data or waits and reads the new value. So one query hits Postgres, not hundreds. Always put a TTL on the lock so that even if the recomputing process dies, the lock doesn't deadlock forever.
2. **Probabilistic early expiration (XFetch).** Each reader, as the TTL nears, randomly decides "should I refresh early?" with a probability proportional to the recompute cost. The refresh gets spread out without ever converging on one instant, so the herd never forms. When recompute is expensive and traffic is very high, this is the most elegant fix.
3. **Add jitter to TTLs.** If you give every key a fixed 10 minutes, they all expire in lockstep — they bring down the house together. Make the TTL `10min ± a few random minutes` and keys expire at different moments, spreading the pressure naturally. One-line change, big payoff.
4. **Serve-stale-while-revalidate so users don't wait.** When a key expires, keep serving the old value and refresh in the background. The user never waits on a recompute; the process holding the lock quietly refreshes the cache. This also drops the "waiters" cost of the lock approach to zero.

**Bottom line:** if it were me, I'd start with **single-flight lock + TTL jitter** in most cases — together they cover the vast majority of cases, are operationally simple, and are easy to reason about. Reach for XFetch when recompute is genuinely expensive and traffic is genuinely huge; it's more elegant but subtler to implement and test. And whichever you pick, add serve-stale-while-revalidate: never make a user wait behind a database query.

## Related Reading

- [Why should I use a Sorted Set for a live leaderboard in Redis?](https://www.muhammetsafak.com.tr/en/just-ask/redis-data-types-using-sorted-sets-for-a-leaderboard/) — Just Ask
- [Should I use chunkById instead of chunk when the same job also updates the rows it iterates?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-chunkbyid-instead-of-chunk-when-the-same-job/) — Just Ask
- [Should I use a partial index on a queue table where I only ever scan 'pending' rows?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-a-partial-index-on-a-queue-table-where/) — Just Ask
