Surviving the cache miss  / field guide
Practitioner field guide · 2026-09-02

A cache miss is a protocol, not an event

When a hot key vanishes, every concurrent request becomes a candidate to recompute it, and the system needs an answer to three questions: who recomputes, who waits, and what the waiters get. This guide reconstructs that protocol from Facebook, Slack and Wikimedia postmortems, and from the implementations at Google, Instagram, Discord, DoorDash, Netflix, memcached, Rails, nginx, Varnish and Fastly.

24 primary sources 12 production systems 3 postmortems Evidence through Sep 2026 Read: 25 min
01

The territory

The problem, who has solved it in production, and the finding that reorders how you should think about it.

13×
Peak DB query rate cut by memcache leases: 17K/s to 1.3K/s during a stampede
52%
Hit ratio that was enough to push p75 app-server latency from 300 ms to 1500–4000 ms
2.5h
Facebook offline in 2010 after its cache repair path became a query stampede
10s
Minimum interval between recompute tokens per key in Facebook's lease design

Strip the vocabulary away and the problem is this: many requests want the same expensive result at the same moment, the saved copy of that result has just become unavailable or untrustworthy, and something must stop them all from doing the expensive work at once. Engineers call the failure a cache stampede, a dogpile, or a thundering herd; the Facebook memcache paper uses the third term, Rails and Python libraries the second, DoorDash and the VLDB literature the first. All three names describe the same amplification: at a 90% hit ratio the origin sees a tenth of the traffic, so losing the cache multiplies origin load by ten (arithmetic: 1/(1−hit ratio); the general amplification argument is worked through in the retry-storms guide in this collection).

The surprise in the public record, and the reason this guide leads with incidents rather than mechanisms: in the worst published cache outages, the stampede was not triggered by a hot key quietly expiring. It was triggered by the machinery that manages the cache. In Facebook's 2010 outage the code that "fixed" bad cache entries deleted keys and re-queried the database in a loop; the postmortem calls it an unfortunate handling of an error condition, and recovery required turning the site off. In Slack's 2-22-22 incident a routine Consul upgrade caused the cache-management system, Mcrib, to flush and promote nodes until the hit rate fell at peak traffic. Expiry-driven stampedes are real, but they are the well-defended case. The undefended case is your own control plane emptying the cache faster than the origin can refill it.

Figure 1 · Four places the miss protocol can live

hit

miss

Concurrent
requests

Key fresh
in cache?

Serve value

Miss protocol:
who recomputes,
who waits, gets what

In-process library
singleflight, promises
(Google, Instagram, DoorDash)

Cache server protocol
leases, win flags
(Facebook, memcached)

Dedicated tier
Rust data services
(Discord)

Edge proxy or CDN
coalescing, collapsing
(nginx, Varnish, Fastly)

Origin store

hit

miss

Concurrent
requests

Key fresh
in cache?

Serve value

Miss protocol:
who recomputes,
who waits, gets what

In-process library
singleflight, promises
(Google, Instagram, DoorDash)

Cache server protocol
leases, win flags
(Facebook, memcached)

Dedicated tier
Rust data services
(Discord)

Edge proxy or CDN
coalescing, collapsing
(nginx, Varnish, Fastly)

Origin store

Every production system in this guide answers the same miss with machinery at one (or more) of four layers. Sources: groupcache, memcached protocol, Discord, Fastly.
Diagram source

Scope. This guide covers the demand side of look-aside and read-through caches: stampedes, invalidation discipline, and cold starts, with the mechanisms that survive them. It deliberately does not cover eviction and admission algorithms, CPU or browser caches, cache coherence in the hardware sense, or the retry amplification that turns an overloaded origin into a metastable failure; that last topic has its own guide in this collection.

02

How it is actually built

Twelve systems, three primitives, one recurring shape. The implementations disagree about where the machinery lives, not about what it does.

Read the implementations side by side and a common shape emerges. Every one of them is assembled from three primitives, in different mixtures. The sources use different names for each, so this guide names them once and maps the vocabulary.

Let one through. Elect a single recomputer per key and make everyone else not recompute. Facebook's leases do the election inside the cache server: on a miss, memcached hands the client a token, regulates tokens to one per key every 10 seconds, and rejects sets from stale tokens; the NSDI '13 paper reports peak database load during a stampede falling from 17K queries per second to 1.3K. Google's groupcache does it in the client library: the README describes coordinating cache fills so that "only one load in one process of an entire replicated set of processes populates the cache, then multiplexes the loaded value to all callers", and the underlying singleflight package is one mutex, one map and one WaitGroup. Instagram caches the in-flight computation itself: "we cache a Promise that will eventually provide the value", so concurrent misses find the promise and wait on one backend call. Discord moved the same idea into a dedicated Rust tier where the first request spawns a worker and later requests subscribe to its result, with consistent-hash routing by channel ID so requests for the same hot partition land on the same coalescer. nginx (proxy_cache_lock), Varnish (the waiting list) and Fastly (request collapsing, where "only one request actually escapes the data center to go to your origin") are the same election at the HTTP layer.

Lie a little. Give the non-elected requests a stale answer instead of a wait. RFC 5861 standardized this for HTTP in 2010 as stale-while-revalidate, "immediately return a stale response while it revalidates it in the background". Rails has shipped it as one parameter, race_condition_ttl, whose doc comment names the dog pile effect explicitly. memcached's meta protocol marks items stale on invalidation and serves them with an X flag while one winner recaches. MediaWiki's WANCache serves one-second "interim values" during the purge window for the same reason. The staleness tolerance is load-bearing: if your correctness budget is zero staleness, half the toolbox disappears, and you are left with waiting or failing.

Spread the edge. Stop expiries from synchronizing. The VLDB 2015 paper on probabilistic early expiration (XFetch) has each reader independently recompute early with a probability that rises as expiry approaches, shifted by delta·beta·log(rand()); it needs no coordination and, the authors show, the exponential variate is optimal. The Internet Archive built a public harness comparing plain fetch, lock-only, XFetch and XFetch-plus-lock, and its README is blunt: "locked and fetch are susceptible to cache stampede, congestion collapse, and starved workers. They do not scale well." TTL jitter at write time is the degenerate, zero-code version of the same primitive.

Figure 2 · The reference miss path

winner:
lease / lock / first caller

everyone else

serve stale

block

spreads recomputes
before expiry

prevents the
cold-start herd

Concurrent callers

Coalescer
one recompute per key

Recompute
against origin

Waiters'
contract

Stale or interim copy
SWR, race_condition_ttl,
memcached X flag

Wait on the
winner's result

Origin

Repopulate cache

Expiry shaping
TTL jitter, XFetch

Warmer
for new / replaced nodes

winner:
lease / lock / first caller

everyone else

serve stale

block

spreads recomputes
before expiry

prevents the
cold-start herd

Concurrent callers

Coalescer
one recompute per key

Recompute
against origin

Waiters'
contract

Stale or interim copy
SWR, race_condition_ttl,
memcached X flag

Wait on the
winner's result

Origin

Repopulate cache

Expiry shaping
TTL jitter, XFetch

Warmer
for new / replaced nodes

The three primitives compose: shaping reduces how often the coalescer is needed, the coalescer elects a winner, the waiters' contract decides what everyone else gets. Reconstructed from NSDI '13, groupcache, WANCache and Netflix's warmers.
Diagram source

Invalidation is a write, not a delete

WANCache purges by SETting a short-lived tombstone (about 11 seconds) rather than deleting, because a delete "could be re-populated immediately with the same stale value" by a racing reader in a replicated deployment. memcached's meta delete grew an equivalent: the I flag marks stale and bumps CAS instead of removing.

Runs this way at: Wikimedia, memcached meta protocol

Cold nodes are a capacity event

Netflix treats a new or replaced cache replica as something to fill before it serves: a replica warmer for scaling events, an instance warmer for replacements, moving petabytes over multi-attach EBS. Slack's incident is the counterexample: nodes rejoined empty at peak and the fleet's hit rate slid.

Runs this way at: Netflix; counterexample Slack

TTL does more work than eviction

Twitter's OSDI '20 study of 153 production cache clusters (80 TB of traces) found TTL "an important and sometimes defining parameter of cache working sets", and many workloads far more write-heavy than the literature assumed. Your TTL distribution is your expiry schedule, which is to say your stampede schedule.

Source: Yang et al., OSDI '20, traces at twitter/cache-trace

Where the implementations diverge is placement, and the divergence is explained by who the callers are. A single-language service can use an in-process library (Go's singleflight, Kotlin coroutines at DoorDash, folly futures at Instagram). A polyglot fleet cannot share a library, which pushes the election into the cache protocol (Facebook's leases, now memcached's meta commands) or into a tier that owns the database (Discord, whose Rust data services sit between the API monolith and ScyllaDB precisely so coalescing happens once, regardless of caller language). At the HTTP boundary the proxy already sees every request, so the edge products ship it as configuration. Inferred rather than reported: nobody in this corpus runs only one layer; Facebook has leases in the cache and pools in the client, Discord has coalescing in the tier and caching inside it. Defence in depth is the norm.

Figure 3 · Lifecycle of a protected key

winner sets value with TTL

soft TTL passes, or invalidate marks stale

one winner recomputes and sets

other readers serve the stale copy

purge on source write

hold-off expires, winner recomputes

interim value serves readers

hard TTL passes

miss, election, one recompute

Fresh

Stale

Tombstoned

Missing

winner sets value with TTL

soft TTL passes, or invalidate marks stale

one winner recomputes and sets

other readers serve the stale copy

purge on source write

hold-off expires, winner recomputes

interim value serves readers

hard TTL passes

miss, election, one recompute

Fresh

Stale

Tombstoned

Missing

The states a key passes through when invalidation marks stale instead of deleting, per memcached's meta protocol and WANCache's tombstones. Note that readers are never left with nothing until the hard TTL passes.
Diagram source
03

The decisions that matter

Each fork with the condition that flips it. The waiters' contract is the one that gets skipped, and it is the one the incidents keep landing on.

Decision: what do the non-elected requests get?

Chosen (most systems)
  • Serve stale while one refreshes: RFC 5861 for HTTP, race_condition_ttl in Rails, X-flag staleness in memcached, interim values in WANCache
  • Won because a seconds-old value is almost always acceptable and it keeps latency flat through the refresh
Rejected (as a default)
  • Make waiters block on the winner. nginx's proxy_cache_lock without use_stale does this, and one practitioner account notes all requests arriving during the update "essentially wait"
  • Blocking converts a stampede into a latency wall on every refresh of a hot key
Flips when
  • Staleness is a correctness bug (authorization state, balances, inventory holds): then waiters must block or fail fast, and you size the wait with a timeout
  • Recompute is so fast that blocking is invisible; then the simplicity wins

Decision: on invalidation, delete the key or mark it stale?

Chosen (replicated deployments)
  • Mark stale, keep the bytes: WANCache tombstones (a purge is a SET), memcached md+I (bump CAS, serve with X flag)
  • Won because the stale copy is the shock absorber for the recompute window
Rejected
  • Plain DELETE. Wikimedia's stated reason: a racing reader can immediately re-populate the key "with the same stale value that was just deleted"
  • Facebook 2010 is the extreme case: delete wired to an error path produced the stampede
Flips when
  • Single node, no replication lag, strict freshness: delete is simpler and the repopulation race mostly disappears
  • The value is large and memory pressure is the binding constraint

Figure 4 · Choosing a miss protocol

yes

no

yes

no

yes

no

Can callers tolerate
a slightly stale value?

Serve stale, refresh async:
SWR / race_condition_ttl /
memcached mark-stale

Is the recompute expensive,
or the origin fragile?

Elect one recomputer,
waiters block with timeout:
lease, lock, singleflight

Let misses through;
add TTL jitter and an
origin concurrency cap

Hot keys with
steady traffic?

Add probabilistic early
refresh: XFetch, beta 1.0

TTL jitter alone
is usually enough

yes

no

yes

no

yes

no

Can callers tolerate
a slightly stale value?

Serve stale, refresh async:
SWR / race_condition_ttl /
memcached mark-stale

Is the recompute expensive,
or the origin fragile?

Elect one recomputer,
waiters block with timeout:
lease, lock, singleflight

Let misses through;
add TTL jitter and an
origin concurrency cap

Hot keys with
steady traffic?

Add probabilistic early
refresh: XFetch, beta 1.0

TTL jitter alone
is usually enough

Terminal nodes are mechanisms you can ship this quarter. The first question does most of the work; answer it with the product owner, not in the cache library.
Diagram source
DecisionChosenRejectedBecauseFlips whenEvidence
Where the coalescer livesIn-process library (Go, Kotlin, folly) Cache-server or tier machineryOne language, one process model; a library is a day of work Fleet goes polyglot or a hot partition needs one coalescer: push into the protocol (leases) or a tier (Discord) groupcache, Discord
Waiters' contractServe staleBlock on winner Latency stays flat through refresh Staleness is a correctness bug: block with timeout, or fail fast RFC 5861, Rails
InvalidationMark stale / tombstone SETDELETE Delete races with repopulation under replication lag Single node and strict freshness WANCache, memcached md+I
Expiry shapingProbabilistic early refreshLocks alone No coordination; the Internet Archive's harness found lock-only strategies starve workers Key traffic is sparse: there is no reader to volunteer early, so jitter at write time instead VLDB '15, IA harness
Cold replicasWarm before servingServe cold, let misses fill it A cold node at peak is a herd generator Dataset refills from misses inside the latency SLO (small, cheap values) Netflix, Slack
Uncacheable responses under coalescingHit-for-pass markerTTL 0 and hope Waiters cannot share an uncacheable response; Varnish then serializes them one by one Never; this one is unconditional if you run coalescing Varnish docs
The pattern behind the table

Every mechanism here is a composition of the same three primitives: elect one recomputer, give waiters a stale answer, spread the expiry edge. When you evaluate a cache product or a library, ask which of the three it implements and what it does for the other two. A product that only locks has decided your waiters' contract for you, and decided it badly.

04

What broke in production

Three published incidents and one documented failure mode, grouped into the three classes the record supports.

The incidents in this corpus sort into three classes. Class one, the repair path is the stampede: the machinery that corrects cache state does the damage. Class two, the cliff is above zero: a partial hit-ratio dip, not a wipe, is enough to saturate the origin. Class three, the protection backfires: coalescing or locking itself becomes the bottleneck. No published postmortem in this corpus describes a plain TTL expiry of a single hot key taking a site down, which is worth pausing on: either the classic textbook stampede is well-defended by now, or it dies too quickly to earn a postmortem. Both readings say the exotic cases in this section are the ones to design for.

Figure 5 · Facebook 2010: the repair loop that outran the fix

"Config DB cluster""Cache""App clients""Config DB cluster""Cache""App clients"invalid config valuecached everywhereerror handled asanother invalid valueloop sustains hundreds ofthousands of queries per secondread configvalue fails validationdelete keyquery fresh valueerror, cluster overloadeddelete key againquery again
"Config DB cluster""Cache""App clients""Config DB cluster""Cache""App clients"invalid config valuecached everywhereerror handled asanother invalid valueloop sustains hundreds ofthousands of queries per secondread configvalue fails validationdelete keyquery fresh valueerror, cluster overloadeddelete key againquery again
The feedback loop per Facebook's postmortem: clients interpreted database errors as more cache corruption, deleted more keys, and sustained the load after the bad value was already corrected.
Diagram source
Postmortem

Facebook, 2010: the error path deleted the cache

AssumptionAn automated checker that replaces invalid cached config values with fresh DB reads makes the system self-healing.
What happenedAn invalid value entered the persistent store, so every client "saw the invalid value and attempted to fix it". The fix queried a DB cluster, which was "quickly overwhelmed by hundreds of thousands of queries a second". Each resulting error was itself treated as an invalid value: the client "interpreted it as an invalid value, and deleted the corresponding cache key", so the herd was self-sustaining after the root cause was fixed.
Blast radiusSite down or unreachable roughly 2.5 hours; worst outage in over four years at the time.
FixStop all traffic to the DB cluster (turn the site off), then redesign to make the repair path unable to amplify.
Design ruleNever wire "on error, invalidate and refetch" without a budget. The repair path needs the same election and rate limits as the miss path, because under stress it IS the miss path.
Postmortem

Slack, 2-22-22: the cache manager emptied the fleet at peak

AssumptionA 25%-at-a-time Consul upgrade is routine; two prior 25% steps had passed without incident.
What happenedAgent restarts made memcached nodes leave the catalog; Mcrib replaced each with a spare, and "the new cache node will be empty", flushing nodes that left and rejoined. Hit rate fell as daily peak arrived. Misses landed on the worst query shape: client boot fetches GDM membership, which is sharded by user, so finding members of one GDM means "you have to query every shard in the datastore". Vitess scatter queries at peak tipped the datastore into cascading failure.
Blast radiusUsers unable to connect or degraded through the morning peak; a significant portion of the cache unavailable, "requiring most users to query every shard".
FixCache-management changes so node replacement does not flush warm data, plus attention to the scatter-query shape that misses fell onto.
Design ruleModel your cache control plane as a load generator: every action it takes (flush, promote, replace) has an origin-side cost, and that cost arrives correlated, at whatever time the control plane acts.
Postmortem

Wikimedia, 2020: a 52% hit ratio was the whole incident

AssumptionA cache in front of a slow lookup keeps the app servers healthy; a partial dip in one extension's hit ratio is a nuisance, not an outage.
What happenedFor nine minutes the WANCache hit ratio for Babel language-data keys dropped to about 52%. Each miss made an HTTPS API call that "hung for ten seconds waiting for a response before timing out", tying up app-server workers and CPU.
Blast radiusp75 app-server latency rose from ~300 ms to 1500–4000 ms across the site; recovery was immediate when the hit ratio returned.
FixActionables around the extension's caching and the timeout behaviour of the fallback path.
Design ruleThe cliff is above zero. Capacity-plan the origin and the worker pool for your worst tolerated hit ratio, not for hit-ratio zero, and pair every cached lookup with a timeout far below the worker-starvation threshold.
Documented failure mode

Varnish: coalescing serializes on uncacheable responses

AssumptionRequest coalescing is pure upside: one origin fetch, every waiter shares the response.
What happenedIf the response comes back uncacheable, "the waiting requests can't reuse that response, so Varnish sends the first request from the waiting list to the backend again", and the queue drains one request at a time: request serialization. The protective queue becomes a convoy.
Blast radiusNot an incident report but a chronic production trap the vendor documents and warns about; latency multiplies by queue depth for the affected object.
FixHit-for-pass: cache the fact that the object is uncacheable, so subsequent requests "bypass the waiting list to avoid serialization". Never express "don't cache" as TTL 0.
Design ruleAny coalescer needs an explicit answer for the response that cannot be shared. If you build one in-house, hit-for-pass is a requirement, not an optimization.
SourceVarnish Software docs, checked 2026
05

Numbers you can plan against

Everything quantitative in the corpus, dated. Amplification math is derived and shown; everything else is measured or claimed by the source named.

MetricValueAtContextAs ofSource
Peak DB rate during stampede, no leases17K queries/sFacebookMeasured over a week; same workload with leases below2013NSDI '13
Peak DB rate with leases1.3K queries/sFacebookLease token issued at most once per 10 s per key2013NSDI '13
Cache tier scale>1B requests/sFacebook"trillions of items"; memcache fleet-wide2013NSDI '13
Cache tier scale~30M requests/sNetflixSpeaker's figure: EVCache peak, hundreds of billions of objects, tens of thousands of instances2016Strange Loop talk
Production trace corpus153 clusters / 80 TBTwitter54 clusters released publicly, 14 TB uncompressed2020OSDI '20, traces
Hit-ratio dip that caused an incident52%WikimediaNine minutes; one extension's keys; p75 latency 300 ms to 1500–4000 ms2020Incident doc
Stampede-driven load during outage100Ks queries/sFacebook"hundreds of thousands of queries a second" against one DB cluster; site off ~2.5 h2010Postmortem
Tombstone hold-off / interim TTL11 s / 1 sWikimediaPurge survives replication lag; interim value absorbs regeneration herdchecked 2026WANCache design
Origin amplification from cache loss1/(1−hit)derived90% hit ratio means 10x origin load if lost; arithmetic, not measurementderived, this guide
XFetch tuning burdenbeta = 1.0VLDB '15Authors: "requires no parameter tuning"; exponential variate proven optimal2015paper
Read these carefully

Measured: the Facebook lease figures, the Wikimedia latencies, the Twitter trace corpus. Claimed: the Netflix talk-abstract scale figures and Facebook's fleet-wide request rate, both self-reported without an audit trail. Derived: the amplification row, arithmetic shown. Unknown, and nobody publishes it: how often stampede protection actually fires in production, and what stampedes cost in dollars. If you instrument one number after reading this page, make it lease-wait or coalescer-queue time; no public source reports it.

06

The evidence wall

Every source behind this page, graded. The ledger with per-claim quotes ships alongside as sources.md.

Postmortem Facebook2010-09

More Details on Today's Outage

The canonical cache-stampede incident: an automated repair path deleted keys on every database error, sustaining hundreds of thousands of queries per second after the root cause was fixed. Recovery meant turning the site off.

Carry forwardThe repair path is a miss path; give it the same election and rate limits.
engineering.fb.com
Postmortem Slack2022

Slack's Incident on 2-22-22

A Consul upgrade made cache nodes rejoin empty at daily peak; misses fell onto a cross-shard scatter query and Vitess cascaded. A complete anatomy of a control-plane triggered stampede.

Carry forwardCache management actions are load events; schedule and bound them like deploys.
slack.engineering
Postmortem Wikimedia2020-02

Incidents/2020-02-04 app server latency

Public incident doc with the rare thing: exact hit-ratio and latency numbers. A 52% hit ratio on one key family for nine minutes multiplied site-wide p75 latency by 5 to 13.

Carry forwardPlan origin and worker capacity for the worst tolerated hit ratio, not for average.
wikitech.wikimedia.org
Source memcachedmaster, 2026

doc/protocol.txt: meta commands

Mainline memcached's lease semantics, in the protocol itself: N and R flags let one client "win" the recache, W/X/Z tell everyone else to serve stale, retry or wait. Written against "dog piling" by name.

Carry forwardYou no longer need Facebook's fork; the anti-stampede protocol is in stock memcached.
github.com/memcached
Source Google2013

golang/groupcache README

The design document for library-side coalescing: one load per replicated process set, multiplexed to all callers, with automatic mirroring of super-hot keys. Deliberately no delete, no expiry, no versioned values.

Carry forwardCoalescing plus hot-key replication covers reads; the features it refuses are the invalidation problems it avoids.
github.com/golang/groupcache
Source Google2013

singleflight/singleflight.go

The whole election mechanism in about 60 lines: a mutex, a map of in-flight calls, a WaitGroup. Duplicate callers wait and share the winner's result and error.

Carry forwardNote it shares errors too: one failing recompute fails every waiter. Decide if that is what you want.
singleflight.go
Source Railsmain, 2026

ActiveSupport::Cache race_condition_ttl

Serve-stale-while-one-regenerates as a single fetch parameter, in a mainstream framework since 2011: bump the expired entry's TTL, let one process regenerate, others read the stale value. The doc comment names the dog pile effect.

Carry forwardThe API shape to copy: staleness budget as an explicit caller-supplied number.
rails/activesupport/cache.rb
Source Twitter2020-03

twitter/cache-trace

A week of production traces from 54 cache clusters, 14 TB uncompressed, CC-BY. The only public dataset large enough to test a stampede-protection design against real key popularity and TTL distributions.

Carry forwardBenchmark your miss protocol against these traces before trusting a synthetic load test.
github.com/twitter/cache-trace
Source Go project2022, open

golang/go #53427: generic singleflight

The proposal to add type parameters to singleflight, still open with no attached PRs. Thirteen years in, the de-facto standard coalescer remains in x/sync with an interface{} API that the community keeps re-wrapping.

Carry forwardExpect to own a thin wrapper; the ecosystem's primitive is stable but frozen.
github.com/golang/go/issues/53427
Decision record IETF2010-05

RFC 5861: stale-while-revalidate, stale-if-error

The waiters' contract, standardized for HTTP: return the stale response immediately, revalidate in the background, and serve stale on origin error to trade freshness for availability.

Carry forwardTwo response-header tokens buy stampede resistance at every conforming cache between you and the user.
datatracker.ietf.org/rfc5861
Decision record Wikimediachecked 2026

Memcached for MediaWiki: WANCache design

The most complete public write-up of invalidation discipline: purges are SETs of 11-second tombstones because deletes race with stale repopulation under replication lag; 1-second interim values absorb the regeneration herd.

Carry forwardThe tombstone TTL equals your replication lag budget; make both explicit.
wikitech.wikimedia.org
Paper Facebook2013-04

Scaling Memcache at Facebook (NSDI '13)

Introduces leases, which solve stale sets and thundering herds with one token: issued at most every 10 seconds per key, verified on set. Measured effect on one stampede-prone workload: peak DB queries 17K/s down to 1.3K/s.

Carry forwardRate-limiting the right to recompute is the strongest lever in the corpus, and it lives server-side.
usenix.org, NSDI '13
Paper Twitter / CMU2020-11

In-memory cache clusters at Twitter (OSDI '20)

153 production clusters analysed: TTL often defines the working set more than eviction does, many workloads are write-heavy, and FIFO is a surprisingly strong replacement policy in production.

Carry forwardYour TTL distribution is a first-class design artifact; audit it before tuning eviction.
usenix.org, OSDI '20
Paper Vattani, Chierichetti, Lowenstein2015

Optimal Probabilistic Cache Stampede Prevention (VLDB '15)

XFetch: each reader recomputes early with probability rising near expiry, shifted by delta·beta·log(rand()). No locks, no coordination, no tuning; the exponential form is proven optimal.

Carry forwardTen lines of code that remove the synchronized expiry edge on steadily-read hot keys.
vldb.org, PVLDB vol 8
Eng blog Instagram2019-04

Thundering Herds & Promises

Cache the in-flight computation: on a miss, insert a promise, and concurrent misses wait on it instead of dialling the backend. Motivated by empty-cache cluster turn-up, the cold-start herd in its purest form.

Carry forwardCaching the promise instead of the value collapses the herd inside one process with no server support.
instagram-engineering.com
Eng blog DoorDash2018-08

Avoiding Cache Stampede at DoorDash

The multi-layer subtlety: an L1 in-process miss stampedes L2 and the database with parallel duplicate reads even when L2 is healthy. Solved with a coroutine debounce returning one shared Deferred per key.

Carry forwardEach cache layer needs its own coalescer; a healthy L2 does not protect itself from your L1 misses.
careersatdoordash.com
Eng blog Discord2023-03

How Discord Stores Trillions of Messages

Request coalescing promoted to its own Rust tier between the API and ScyllaDB: first request spawns a worker, later ones subscribe; consistent-hash routing by channel ID sends a hot channel's requests to one coalescer.

Carry forwardAbove one language and one hot partition, the coalescer wants to be a service with a routing key.
discord.com/blog
Eng blog Netflix2018-12

Cache warming: Agility for a stateful service

Replica warmer for scale-ups, instance warmer for replacements: EVCache nodes are filled from live replicas before they take traffic, so the fleet's hit ratio never dips on a topology change.

Carry forwardTreat "node enters serving" as a gated state transition, not a side effect of registration.
netflixtechblog.com
Eng blog Netflix2021-11

Cache warming: Leveraging EBS for petabytes

The warming pipeline at scale: dumpers write to multi-attach EBS volumes, populators stream into the new replicas. Warming is real infrastructure with its own controller, queues and failure modes.

Carry forwardBudget warming as a data-transfer project: petabyte fleets cannot be refilled through the miss path.
netflixtechblog.medium.com
Eng blog Madhur Ahuja2016-12

proxy_cache_lock and proxy_cache_use_stale, closely read

The practitioner account of nginx's defaults: with the lock on and use_stale off, every request arriving during a refresh waits. The two directives only make sense enabled together.

Carry forwardElection without a staleness valve is a latency wall; audit your proxy config for the pair.
madhur.co.in
Eng blog Data Center Knowledge2010-09

Technical Details of Facebook Outage

Contemporary coverage fixing the blast radius of the 2010 incident: roughly 2.5 hours down or unreachable, the worst outage in over four years.

Carry forwardCorroboration for the duration figure the postmortem describes qualitatively.
datacenterknowledge.com
Vendor nginxchecked 2026

ngx_http_proxy_module: cache_lock, use_stale

The reference semantics: proxy_cache_lock admits "only one request at a time" to populate an element; use_stale's updating parameter serves the stale copy during refresh. Both default off.

Carry forwardThe defaults are the stampede; protection is opt-in and two directives wide.
nginx.org docs
Vendor Varnish Softwarechecked 2026

Under the hood: waiting list and serialization

Varnish's coalescing queue, and its documented trap: uncacheable responses drain the waiting list one at a time. Hit-for-pass exists to bypass the queue for objects known uncacheable.

Carry forward"Do not cache" must be a cached fact, or your coalescer serializes.
docs.varnish-software.com
Vendor Fastlychecked 2026

Request collapsing

CDN-scale coalescing is hierarchical: fetches for an object are focused through one cache node per data center, and its queue lets a single request escape to origin.

Carry forwardA layer of shielding in front of your origin buys the election without touching application code.
fastly.com documentation
Talk Netflix / Strange Loop2016-09

Caching at Netflix: The Hidden Microservice

Scott Mansfield's abstract puts numbers on the cache-as-a-service model: about 30 million requests per second at peak, hundreds of billions of objects, tens of thousands of memcached instances.

Carry forwardAt this scale the cache is a product with its own team; the miss protocol is its API contract.
thestrangeloop.com
Talk Internet Archive / RedisConf2017-05

Preventing Cache Stampede with Redis and XFetch

Jim Nelson's talk ships with a public harness comparing fetch, locked, xfetch and xlocked. Conclusion in the README: plain fetch and lock-only strategies "do not scale well"; xfetch plus a lock had zero misses and no duplicate recomputes.

Carry forwardAn independent adopter reproducing the paper's result; the combination beats either primitive alone.
github.com/internetarchive/xfetch
07

Build a miniature, then productionise it

Seven rungs from reproducing the herd to operating the protection. The line from toy to real is crossed at rung five.

Reproduce the stampede

A service with one cached endpoint (any KV store, 10 s TTL) backed by a query you make artificially slow (500 ms). Load it with a few hundred concurrent clients and graph origin queries per second across an expiry.

Done when: you can point at the origin-QPS spike at each expiry edge.  Teaches: the herd is periodic and self-synchronizing, not random.

Add an in-process coalescer

Wrap the miss path in singleflight (or a promise map in your language). Same load.

Done when: origin sees one query per key per expiry regardless of client count.  Teaches: election, and that the coalescer shares errors as well as values.

Give waiters a staleness budget

Add a soft TTL: past it, serve the old value and refresh in the background; past the hard TTL, block. Copy the Rails race_condition_ttl shape.

Done when: p99 latency is flat through a refresh cycle.  Teaches: the waiters' contract is a caller-visible API decision, not cache internals.

Spread the expiry edge

Implement XFetch: store the recompute duration with the value, refresh when now − delta·beta·log(rand()) passes expiry.

Done when: the origin-QPS graph shows recomputes scattered before expiry, no cliff.  Teaches: coordination-free smoothing, in about ten lines.

Break your own repair path

Make the origin return errors for 60 seconds under load. Watch what your miss and invalidation logic does. Add stale-if-error and negative caching with a short TTL, and rate-limit any delete-on-error behaviour.

Done when: an origin brownout does not increase origin traffic.  Teaches: Facebook 2010; the error path is where stampedes are born.

Run the cold-start drill

Flush 25% of cache capacity while at steady load, in homage to Slack's 2-22-22. Measure time-to-recover and origin peak. Then add either warming (prefill from a peer) or an admission throttle for cold nodes.

Done when: the drill's origin peak stays under your origin's measured capacity.  Teaches: the cliff above zero, and what warming actually buys.

Instrument the protocol, not just the ratio

Emit per-key-family hit ratio, coalescer queue depth and wait time, stale serves per second, and refresh outcomes. Alert on hit-ratio dips and on coalescer wait, with a runbook that names the cold-fleet procedure.

Done when: a game-day hit-ratio dip pages before latency does.  Teaches: the leading indicators; no public source ships these numbers, so yours are the benchmark.

08

Keep hunting

The queries that found this material, grouped by what they surface. The domain vocabulary (stampede, dogpile, thundering herd, coalescing, collapsing, lease) is the key; each community uses a different word for the same mechanism.

Incidents and postmortems

  • "cache stampede" postmortem OR "incident review" OR "root cause"
  • site:wikitech.wikimedia.org incident memcached outage
  • site:slack.engineering incident cache
  • "thundering herd" outage "queries per second"

Mechanisms in source and specs

  • memcached protocol.txt "dog piling" won recache stale
  • singleflight proposal site:github.com/golang
  • race_condition_ttl dog pile site:github.com/rails
  • varnish "waiting list" serialization "hit-for-pass"

Engineering accounts

  • "thundering herds" promises instagram engineering
  • "request coalescing" "data services" discord rust
  • "cache warming" replica warmer netflix EVCache
  • "cache stampede" "we" debounce coroutines

Papers and measured numbers

  • "optimal probabilistic cache stampede prevention" vldb
  • memcache leases "peak query rate" 17K nsdi
  • osdi 2020 twitter cache clusters TTL working set
  • twitter/cache-trace production traces github
09

References

  1. Facebook, More Details on Today's Outage engineering.fb.com, 2010-09-23. Checked 2026-09-02.
  2. Data Center Knowledge, Technical Details of Facebook Outage datacenterknowledge.com, 2010-09. Checked 2026-09-02.
  3. Slack, Slack's Incident on 2-22-22 slack.engineering, 2022. Checked 2026-09-02.
  4. Wikimedia, Incidents/2020-02-04 app server latency wikitech.wikimedia.org, 2020. Checked 2026-09-02.
  5. memcached, doc/protocol.txt (meta commands) github.com, master branch. Checked 2026-09-02.
  6. Google, golang/groupcache README github.com, 2013 onward. Checked 2026-09-02.
  7. Google, singleflight/singleflight.go github.com, 2013. Checked 2026-09-02.
  8. Rails, activesupport cache.rb (race_condition_ttl) github.com, main branch. Checked 2026-09-02.
  9. Twitter, cache-trace github.com, traces of 2020-03. Checked 2026-09-02.
  10. Go project, proposal: x/sync/singleflight generic version (#53427) github.com, opened 2022-06, open. Checked 2026-09-02.
  11. Nishtala et al., Scaling Memcache at Facebook USENIX NSDI, 2013-04. Checked 2026-09-02.
  12. Yang, Yue, Rashmi, A large scale analysis of hundreds of in-memory cache clusters at Twitter USENIX OSDI, 2020-11. Checked 2026-09-02.
  13. Vattani, Chierichetti, Lowenstein, Optimal Probabilistic Cache Stampede Prevention PVLDB 8(8), 2015. Checked 2026-09-02.
  14. Nottingham, RFC 5861: HTTP Cache-Control Extensions for Stale Content IETF, 2010-05. Checked 2026-09-02.
  15. Wikimedia, Memcached for MediaWiki (WANCache design) wikitech.wikimedia.org, current. Checked 2026-09-02.
  16. Nick Cooper, Thundering Herds & Promises Instagram Engineering, 2019-04. Checked 2026-09-02.
  17. Zohaib Hassan, Avoiding Cache Stampede at DoorDash DoorDash Engineering, 2018-08. Checked 2026-09-02.
  18. Discord, How Discord Stores Trillions of Messages discord.com/blog, 2023-03. Checked 2026-09-02.
  19. Netflix, Cache warming: Agility for a stateful service Netflix TechBlog, 2018-12-04. Checked 2026-09-02.
  20. Netflix, Cache warming: Leveraging EBS for moving petabytes of data Netflix TechBlog, 2021-11-26. Checked 2026-09-02.
  21. Madhur Ahuja, Close look at proxy_cache_lock and proxy_cache_use_stale in Nginx madhur.co.in, 2016-12-25. Checked 2026-09-02.
  22. nginx, ngx_http_proxy_module documentation nginx.org, current. Checked 2026-09-02.
  23. Varnish Software, Under the hood docs.varnish-software.com, current. Checked 2026-09-02.
  24. Fastly, Request collapsing fastly.com documentation, current. Checked 2026-09-02.
  25. Scott Mansfield, Caching at Netflix: The Hidden Microservice Strange Loop, 2016-09. Checked 2026-09-02.
  26. Jim Nelson / Internet Archive, xfetch test harness (RedisConf 2017) github.com, 2017. Checked 2026-09-02.