ISSUE 42
System Design

Your Cache Expired and the Database Spiked: Four Fixes for Cache Stampedes

An analysis of cache stampede mechanics and four robust architectural patterns to prevent catastrophic database load during key invalidation events.

Abhik Kumar Panda
Abhik Kumar Panda
Creator & Engineer
August 25, 2026 · 3 min read
A visualization of a distributed system experiencing a traffic spike due to cache expiration

In distributed systems, the cache-aside pattern is often the default choice for reducing database latency. However, this pattern carries an inherent risk: the cache stampede. When a high-traffic key expires, multiple concurrent requests observe the cache miss simultaneously and all attempt to regenerate the value by querying the underlying database. This ‘thundering herd’ effect can push a database to its limit, potentially triggering a cascading failure across your infrastructure.

A stampede is not merely a performance annoyance; it is a structural vulnerability. Because the database is often the most constrained resource, allowing it to handle concurrent, redundant requests for the same computation is a failure of system design. Below, we examine four strategies to decouple cache invalidation from database load.

1. External Locking (Mutex)

The most straightforward approach is to ensure that only one process is responsible for recomputing the cache entry. By implementing a distributed lock—typically using Redis SETNX or a similar atomic primitive—you force concurrent requests to wait while a single worker refreshes the cache.

async function getCachedData(key: string) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const lockKey = `lock:${key}`;
  const acquired = await redis.set(lockKey, 'true', 'NX', 'EX', 10);

  if (acquired) {
    const data = await db.query(key);
    await redis.set(key, JSON.stringify(data), 'EX', 3600);
    await redis.del(lockKey);
    return data;
  }

  // Wait and retry
  await sleep(100);
  return getCachedData(key);
}

While effective, this introduces a dependency on your locking mechanism. If the process holding the lock dies, you must have robust timeout handling to prevent deadlocks.

2. Probabilistic Early Recomputation

Instead of waiting for the cache to hit zero, you can trigger a refresh before the expiration time. Using probabilistic early recomputation, each request calculates a ‘refresh probability’ based on the remaining TTL. As the TTL approaches zero, the likelihood that a request will trigger a background refresh increases.

This method effectively spreads the recomputation load over a period of time, ensuring that the cache is ‘warm’ before it officially expires. It is particularly useful for keys with very high read volume where even a short lock duration is undesirable.

3. The Background Refresh Pattern

Rather than relying on the request path to handle recomputation, move the work to a background worker. In this model, the cache never truly expires; it simply becomes stale. A background process periodically polls the database and updates the cache, ensuring that end-users always receive a cached response.

The background refresh pattern shifts the complexity from the request-response cycle to your asynchronous task infrastructure, effectively decoupling user experience from database latency spikes.

4. Stale-While-Revalidate

Borrowing a concept from HTTP headers, you can configure your application to serve stale data while simultaneously triggering an asynchronous update. When a cache miss occurs, the system immediately returns the expired data (if available) and dispatches a job to update the cache for the next request.

  • Pros: Near-zero latency for the end user.
  • Cons: Users may receive slightly outdated information for a few seconds.
  • Best for: Systems where eventual consistency is acceptable.

Trade-offs and Operational Reality

No single strategy is a panacea. Distributed locking adds latency to the request path and introduces a new failure point. Probabilistic recomputation is elegant but harder to reason about during debugging. Background refreshing requires a reliable task queue and monitoring for worker health.

When choosing a solution, evaluate the cost of a stale read versus the cost of a database outage. If your database is the bottleneck, prioritizing availability through stale-while-revalidate is often the superior engineering choice.

Conclusion

Cache stampedes are a classic distributed systems problem that serves as a litmus test for infrastructure maturity. By moving away from naive cache-aside implementations and adopting patterns like probabilistic recomputation or asynchronous background updates, you can build systems that remain resilient even under intense read pressure. The goal is to treat the database as a durable source of truth, not as a scratchpad for every cache miss.

Share Twitter LinkedIn
Abhik Kumar Panda
CONTRIBUTING FELLOW

Abhik Kumar Panda

Creator & Engineer

Software engineer and creator passionate about technical writing, systems architecture, and AI.

Continue Reading

Stay in the loop.

Get new engineering articles, systems ideas, and things worth knowing — straight to your inbox.

No spam. Just useful things.
Unsubscribe anytime.