Thundering herd and cache stampede simulator

When a popular cached value expires, every request that arrives before it has been rebuilt goes to the database at once. Set the request rate, TTL and recompute time to see how big that stampede gets, then compare the four standard fixes on the same traffic.

Recomputes
1,970
Most at once
224
Per refresh
197
On a miss

every request that misses recomputes the value itself.

Recomputes running at once, over 12 seconds
0 s3 s6 s9 s12 s2240

Each refresh triggered 197 identical recomputes: everything that arrived during the 100 ms it took to rebuild the value missed too.

All four on this traffic
StrategyRecomputesPeakWaited
No protection1,9702241,970
Lock (single flight)1011,970
Serve stale1010
Early expiration (XFetch)2210
Requests
23,971
Refreshes
10
Longest wait
100 ms
Stale answers
0

How to use the simulator

Requests arrive at random at the rate you set, spread evenly over the hot keys. Every key starts cached, as just after a cache warm-up, and expires after its TTL; rebuilding it takes the recompute time. The chart shows how many recomputes are running at each moment; the table runs all four strategies on the same traffic. Raise the number of keys and add TTL jitter to see expiries that line up and spread out.

What is a thundering herd?

A thundering herd is many waiters woken or released at once for work only one of them needs to do. In caching it is called a cache stampede or dogpile: a popular value expires, the requests that arrive in the next moments all miss, and each one goes to the database or service behind the cache to rebuild the same value. The backend, sized for the cache’s normal hit rate, suddenly sees a burst of identical expensive queries, slows down, and so stretches the window in which more requests miss.

How big a stampede gets

Every request that arrives while the first recompute is still running finds the cache empty, so one expiry causes roughly 1 + rate × recompute time rebuilds. At 2 requests per millisecond and a 100 ms recompute, that is about 200. The simulation of one key with a 1-second TTL over 20 seconds:

StrategyRecomputesPer refreshMost at onceRequests that waitedStale answers
No protection3,580198.92243,5800
Lock (single flight)18113,5800
Serve stale181103,580
Early expiration (XFetch)391100

Four fixes

A lock, or single flight. The first request to miss takes a lock and recomputes; later ones wait for its result instead of starting their own. One recompute per expiry, at the cost of waiting: in the run above 3,580 requests waited an average of 50 ms. Go’s singleflight package does this within one process; across many servers it needs a distributed lock or a lease.

Serve stale while revalidating. Keep the expired value, return it to everyone, and let one request refresh it in the background. Nobody waits, and the backend sees one recompute, but requests get data up to one recompute time older than the TTL promised. HTTP caches can do this with the stale-while-revalidate directive of RFC 5861.

Probabilistic early expiration. In the XFetch algorithm of Vattani, Chierichetti and Lowenstein, each request refreshes the value early when now − Δ·β·ln(rand) ≥ expiry, where Δ is how long the last recompute took and β (1 here unless you change it) tunes how eager it is. Refreshes become likelier as expiry approaches, so usually one request rebuilds the value before it expires and nobody misses. The busier the key, the earlier the refresh: at 2 requests per millisecond it fired about 574 ms before expiry on average, refreshing 38 times in 20 seconds instead of 18. At 0.05 requests per millisecond it refreshed 22 times and 1 requests still met an expired value.

TTL jitter doesn’t help one key, but stops many keys from expiring together, as the next section shows.

Many keys and TTL jitter

Keys written at the same moment with the same TTL expire at the same moment: after a deploy, a cache restart or a bulk load. With 200 hot keys sharing 20 requests per millisecond, a 2-second TTL and a 100 ms recompute, the most recomputes running at once were:

TTL jitterNo protectionWith a lock
none1,970200
± 10%64159
± 30%29729

A lock caps it at one recompute per key, but all 200 keys still rebuild together; jitter spreads the expiries over time. The two fixes address different problems and are usually combined.

The thundering herd in operating systems

The term is older than caching. When many processes block waiting for the same event, such as a connection arriving on a listening socket they share, the kernel can wake all of them though only one can accept the connection; the others wake, find nothing to do and sleep again, wasting a context switch each. Since version 4.5, Linux’s epoll offers the EPOLLEXCLUSIVE flag so that an event wakes one of several waiters instead of all of them.

Thundering herd questions and answers

What is a cache stampede?

When a popular cached value expires, every request that arrives before it has been recomputed misses the cache and recomputes it too. With 2 requests per millisecond and a 100 ms recompute, each expiry in this page’s simulation set off about 199 identical recomputes at once.

How do you prevent a cache stampede?

Make sure only one request recomputes a value: a lock or single-flight call makes the others wait for its result; serving the stale value while one request refreshes it in the background avoids the wait; probabilistic early expiration refreshes the value shortly before it expires; and adding random jitter to TTLs stops many keys from expiring at the same moment.

What is XFetch?

The probabilistic early expiration algorithm of Vattani, Chierichetti and Lowenstein (VLDB 2015). Each request recomputes the value early if now − Δ·β·ln(rand) ≥ expiry, where Δ is how long the last recompute took. The closer the expiry, the likelier a refresh, so one request usually refreshes the value before anyone sees a miss.

Why add jitter to cache TTLs?

Keys written at the same moment with the same TTL expire at the same moment, after a deploy or a cache flush for example. In this page’s run with 200 keys, 1970 recomputes were running at the peak with no jitter, 641 with ±10% and 297 with ±30%.

What is the thundering herd problem in operating systems?

Many processes or threads blocked waiting for the same event, such as a new connection on a shared listening socket, are all woken when it happens, though only one can handle it; the rest go back to sleep having wasted a context switch. Linux added the EPOLLEXCLUSIVE flag in version 4.5 so that epoll wakes only one of the waiters.

Further reading

Published Updated