SD System Design In Depth
0%
lesson system-designcachingconcurrency

Cache concurrency control

Module 4.1: Caching Strategies & In-Memory State Updated Sep 17, 2026
📄 Cheat Sheet
Caching Pitfalls Every Developer Should Know
ByteByteGo Caching Pitfalls Every Developer Should Know Watch on YouTube ↗

A concise ByteByteGo explanation of cache stampede (thundering herd), penetration and avalanche, with locking and early refresh as fixes. It is the core of this lesson in under seven minutes.

0:00 / --:--

Concrete scenario

A product catalog key expires under 10,000 concurrent reads.

Mechanics

Singleflight coalesces duplicate work within one process; cross-process coordination needs a lock, lease, queue, or refresh owner with a bounded wait and failure policy. Probabilistic early refresh such as XFetch reduces synchronized expiry probability but does not guarantee permanent residency. Cache-aside writes still need race handling.

Failure mode

An old reader can load the database, race with an update-and-delete, then repopulate stale data. A refresh owner can crash, waiters can pile up, and a distributed lease can expire while work continues.

Trade-off

Coalescing protects the database but adds waiting and coordination. Versioned values, delayed double deletion, write-through, or stale-while-revalidate each exchange consistency, write latency, complexity, and availability.

Worked example: one hot key, 10,000 readers

A catalogue key takes 200 ms to rebuild from the database and receives 10,000 reads per second across 100 app servers. With no protection, when it expires every request in the 200 ms window misses, which is about 2,000 concurrent identical DB queries.

  • Per-process singleflight: at most one load per server, so 100 concurrent loads. That is 20 times better, but still 100.
  • Distributed rebuild lock (SET lock:key token NX PX 3000): one load cluster-wide. The other callers either serve the stale value or poll briefly.
  • Probabilistic early refresh (XFetch): each reader refreshes early with a probability that rises as expiry approaches, weighted by rebuild cost. Typically one reader refreshes shortly before expiry, and nobody ever sees a miss.

The stale-set race and leases

sequenceDiagram participant Reader participant Cache participant DB participant Writer Reader->>Cache: get k is a miss so lease L1 is issued Reader->>DB: read old value v1 Writer->>DB: update row to v2 Writer->>Cache: delete k which invalidates lease L1 Reader->>Cache: set k to v1 using lease L1 Cache-->>Reader: rejected because lease L1 is no longer valid

Facebook's memcache issues a lease token on a miss. A delete invalidates outstanding leases, so the late set of v1 is rejected. The same token also rate-limits rebuilds: memcache hands out at most one token per key every 10 seconds, and other clients briefly wait and retry.

Technique comparison

TechniqueStops stampedeStops stale setCost
Local singleflightPer process onlyNoTrivial
Distributed lock with TTLYesNoExtra round trip. Stale lock holder
Leases (memcache)YesYesNeeds cache support
Versioned compare-and-setNoYesNeeds version from the DB row
Stale-while-revalidateYes, readers never waitNoServes bounded staleness
XFetch early refreshMostlyNoTiny. Probabilistic
Delayed double deleteNoReduces the windowHeuristic. The delay is a guess
Check yourself Question 1 of 3
Scope of singleflight recall
From: Mechanics

A team wraps cache rebuilds in Go's singleflight and expects exactly one database load per expiring key. The service runs on 200 servers. What actually happens?

Cache Stampede on a Hot Key

One hot key with a TTL; every client reads it about once per second. Rebuilding it takes one database query of fixed duration. All four strategies see the same request stream. With no protection, every request that arrives while the key is being rebuilt also misses and hits the database: roughly request rate × rebuild time queries per expiry.

Architecture Challenge: Cache Stampede & Dogpiling Defense
Advanced
Context / Scenario: Cache stampede / Dog-piling effect on key expiration.
Problem: A celebrity with 50M followers tweets. The cached tweet expires (TTL hits 0). 100,000 requests per second simultaneously miss the cache and hit the database. What prevents this crash?
A Set TTL to 0 so the key is never cached.
B Send an HTTP 500 error to 90% of requests.
C Use Distributed Mutual Exclusion (Mutex lock in Redis) or Probabilistic Early Expiration (XFetch algorithm) so only one worker re-computes the cache.
D Increase PostgreSQL max_connections to 100,000.

Key Takeaways

Singleflight coalesces local work; cross-process refresh needs additional coordination.
Cache-aside delete can race with a stale reader that repopulates the key.
Probabilistic early refresh reduces stampedes but does not guarantee residency.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue