Caching · intermediate
Cache stampedes: identify the expiry boundary that synchronized traffic
Cache stampede: many concurrent callers miss the same cache key at one expiry boundary and synchronously hit the origin, producing a step-shaped origin load spike that a steady miss rate cannot explain. This guide walks a backend engineer from the observable spike to a verified mitigation, without changing live cache state.
The symptoms
- •Origin QPS spikes sharply while cache hit ratio stays near its recent baseline, ruling out a global miss-rate drift
- •Origin p99 latency rises in lockstep with the spike, then drops back, suggesting a sudden burst rather than a sustained backlog
- •Cache miss logs cluster around one specific key rather than spreading across the keyspace
- •Origin connection pool or worker queue saturates, while cache-tier connection counts look normal
- •CPU, lock-wait, or row-contention metrics rise only on queries served by the suspected hot key
- •The spike starts within a small window after a TTL boundary, a deploy, or a memory-pressure eviction event
Likely causes
- •Hot key with a short, fixed TTL expires while many concurrent callers are holding requests for it
- •Cache-aside wrapper has no request coalescing or single-flight, so every caller races to refetch on miss
- •TTL jitter is absent, so per-instance or per-shard timers expire the same key at the same wall-clock instant
- •A rolling restart resets in-process expiry timers fleet-wide, synchronizing the next miss wave
- •Cache eviction under memory pressure evicts a hot key, collapsing many readers onto the origin at once
- •Pre-warm step skipped for a hot key after deploy, so the first wave is fully cold and concurrent
- •Downstream protection (circuit breaker, rate limit) tripped off earlier and was never re-enabled, removing back-pressure that used to throttle the burst
First ten minutes
- 01Open the cache dashboard and the origin dashboard side by side; align timestamps to within seconds so the miss event lines up with the origin spike
- 02Identify the top-N keys by miss count in the surge window and record their configured TTL and any jitter policy
- 03Note the wall-clock start of the origin spike and compare it to the most recent TTL boundary of the top-missed key
- 04Capture origin connection-pool utilization, queue depth, and worker wait time at the surge timestamp before they roll out of retention
- 05Freeze write access to dashboards and config; do not change TTL, eviction policy, or cache contents while evidence is being collected
- 06Page the cache-owner team for a shared timeline of deploys, restarts, and config changes that bracket the surge
Evidence to collect
- •Time-series of cache hits and misses per key covering at least one full TTL interval before and after the surge
- •Origin QPS and latency p50, p95, p99 at second resolution across the surge window
- •Configured TTL, jitter range, and refresh policy for each top-missed key
- •Trace or log samples showing concurrent origin fetches for the same key during the miss window
- •Cache client configuration: connection count, per-call timeout, single-flight or coalescing flag, retry policy
Where to look
- •Cache tier dashboards: hit/miss ratio, eviction counter, key TTL distribution, expired-vs-evicted breakdown
- •Origin tier: connection pool saturation, query latency, lock or row contention, thread-pool queue depth
- •Application logs: cache-wrapper debug lines or span tags such as cache_miss clustered under one parent operation
- •Distributed tracing: spans for the suspected key showing overlapping fetch windows instead of serialized waits
- •Deploy, config-change, and feature-flag history bracketing the surge timestamp
Diagnostic steps
- 01Plot miss count for the suspected key against wall time and look for a step change that aligns with a TTL boundary; this is the stampede fingerprint
- 02Count distinct concurrent in-flight origin fetches for that key during the surge window using traces or structured logs
- 03Compare origin QPS during the spike to QPS immediately before; the ratio approximates the number of callers that raced into the miss
- 04Read the cache wrapper source or config and confirm whether request coalescing or single-flight is enabled and active on this code path
- 05Check whether the key is read by services that share no local in-process cache, which would force them all to consult the shared store on miss
- 06Verify whether TTL is set to a single value or a jittered range, and whether any layer rewrites the TTL on read
- 07Rule out memory-pressure eviction by checking the evicted-keys counter and the allocator's recent pressure metrics
- 08Cross-reference deploy and restart events with the surge start to see if a fleet-wide timer reset synchronized the expiry
Common mistakes
- •Scrolling origin capacity in response to the spike, which hides the synchronized miss instead of removing it
- •Raising TTL on the hot key without first proving the miss is TTL-aligned, masking the real cause behind a longer window
- •Issuing a bulk cache delete to "fix" freshness, which collapses every reader into the cold path and deepens the stampede
- •Adding aggressive retry on miss, multiplying origin load for the same key instead of dampening it
- •Blaming the cache tier when memory-pressure eviction is the actual trigger; the cache is obeying its eviction policy
- •Disabling a circuit breaker that was protecting the origin, removing back-pressure that previously kept the burst survivable
Safe fixes
- •Enable request coalescing or single-flight in the cache-aside wrapper so only one caller fetches the origin value per miss window
- •Add small TTL jitter on the hot key so expiries spread across a window rather than aligning to one instant
- •Introduce probabilistic early refresh so the entry is repopulated before it expires, keeping the miss path cold
- •Wrap the miss path in a short-lived lock with an explicit timeout, scoped to the cache layer and never held across network calls that bypass it
- •Pre-warm known hot keys after deploys or restarts so the first wave is not fully cold and concurrent
Prove the fix
- 01Replay the surge scenario or wait for the next natural expiry, and observe that concurrent origin fetches for the key drop toward one per miss window
- 02Confirm cache hit ratio returns to its prior baseline within one TTL interval after the change is rolled out
- 03Verify origin p99 latency returns to its pre-incident band within the same observation window
- 04Inspect new traces for the key: a single fetch span with peer requests waiting on its result, instead of overlapping fetches
- 05Re-check the TTL distribution for the key and confirm jitter is actually applied rather than overridden by a later write
Prevention and next steps
- •Define a default TTL jitter policy in cache key templates and enforce it in code review for any new key
- •Require single-flight or coalescing in any cache-aside wrapper that reaches production, and unit-test the miss path for it
- •Track miss-concurrency per hot key as an SLO and alert when it exceeds a small threshold above one
- •Run load tests that simulate cold cache and synchronized expiry as part of the standard pre-deploy suite
- •Document a pre-warm checklist for hot keys and run it after any fleet-wide restart or config change to the cache layer
Safe commands and checks
redis-cli -h <cache-host> -p 6379 TTL <key> redis-cli -h <cache-host> -p 6379 --scan --pattern '<key-pattern>' | head -n 20 redis-cli -h <cache-host> -p 6379 INFO stats | grep -E 'keyspace_hits|keyspace_misses|expired_keys|evicted_keys' redis-cli -h <cache-host> -p 6379 CONFIG GET maxmemory-policy ss -tan state established '( sport = :6379 or dport = :6379 )' | wc -l pidstat -p <pid> -r -u 1 5 curl -s http://<metrics-endpoint>/metrics | grep -E 'cache_(hit|miss)|origin_request' | head -n 40 strace -p <pid> -e trace=network -c -f 2>&1 | head -n 40