Thundering Herd: When Cache Expiration Becomes a DDoS


The scenario is deceptively simple: a popular cached value expires, hundreds of concurrent requests suddenly hit your database at once, and your backend collapses under load it was never meant to handle. This is the thundering herd problem, and it turns one of computing’s most fundamental optimizations—caching—into a potential vulnerability.

The Anatomy of a Stampede

Caching works because it trades freshness for speed. Instead of querying a database or running an expensive computation for every request, you store the result and serve it repeatedly until it expires. The problem emerges when that expiration happens under load.

Consider a homepage feed cached for five minutes. At 2:00 PM, the cache warms up with fresh data. At 2:05 PM, the cache entry expires. If 500 requests arrive in the next second, all 500 see a cache miss. Each request independently decides to regenerate the value, triggering 500 concurrent database queries for identical data. The database, sized for normal traffic patterns, cannot handle the sudden spike. Query latency shoots up, connections pool out, and the entire service degrades or crashes.

The herd isn’t just a scaling problem—it’s a positive feedback loop. As the backend slows down, response times increase, keeping more requests in flight. More in-flight requests mean more concurrent cache misses. The system spirals.

Why Traditional Expiration Fails

Most cache implementations use time-to-live (TTL) expiration: set a value, specify how long it should live, and let it disappear when the timer runs out. This works well in low-traffic scenarios, but it has a fatal flaw at scale—deterministic synchronized expiration.

When every cache instance expires the same key at the same moment, every subsequent request experiences a miss simultaneously. The problem compounds with popular keys: the more traffic a cached value serves, the larger the herd when it expires.

Some teams try solving this by increasing TTL duration, but that only delays the problem and forces users to tolerate stale data longer. Others manually warm caches after deployments, but that still leaves synchronized expiration windows.

Probabilistic Early Expiration

One effective mitigation is probabilistic early expiration, sometimes called “jittered TTL.” Instead of waiting for a fixed TTL to elapse, the system calculates a small probability that grows as the entry ages. When checking the cache, even if the value is still technically valid, there’s a chance the system treats it as expired and triggers a refresh.

The probability function typically considers how close the entry is to expiration and how expensive regeneration would be. A value 90% through its TTL might have a 10% chance of early refresh. This spreads regeneration work across time, preventing all requests from missing simultaneously.

The tradeoff is accepting occasional unnecessary regeneration in exchange for avoiding catastrophic synchronized misses. In practice, the overhead is negligible compared to the cost of a stampede.

Request Coalescing and Lock Strategies

Another approach: ensure only one request regenerates an expired value while others wait. When a cache miss occurs, the first request acquires a lock or “lease” on that key. Subsequent requests see the lock and either wait for the first request to complete or return slightly stale data if available.

This requires careful implementation. Locks need timeouts to handle failures—if the regenerating request crashes, the lock must eventually release. Distributed systems need distributed locks, often implemented with Redis or a similar coordination service, which adds latency and complexity.

A lighter-weight variant is request coalescing at the application layer. When multiple in-flight requests target the same key, the application deduplicates them, issues one backend query, and broadcasts the result to all waiting callers. This works well within a single process but doesn’t prevent herds across multiple servers.

Stale-While-Revalidate

Borrowed from HTTP caching semantics, stale-while-revalidate serves expired cached data while asynchronously refreshing it in the background. When a cache entry expires, the first request receives the stale value immediately and triggers a background refresh. Subsequent requests continue receiving the stale value until the refresh completes.

This approach optimizes for availability and latency over strict freshness. Users never wait for regeneration, and the backend sees steady regeneration traffic rather than spikes. The downside is serving outdated data, which may be unacceptable for financial transactions or inventory systems but perfectly fine for news feeds or product catalogs.

Defense in Depth

Production systems typically layer multiple strategies. Probabilistic expiration reduces synchronized misses. Request coalescing limits redundant work. Circuit breakers protect the backend when regeneration starts failing. Rate limiting prevents runaway retry storms.

The thundering herd isn’t just a caching problem—it’s a reminder that optimization strategies carry failure modes of their own. Every system that trades latency for efficiency needs a plan for when that efficiency suddenly disappears.