CMD Guide
HomeSystem DesignCaching

Caching Challenges

A cache earns its keep only while two invariants hold: its hit rate stays high (so the origin sees a trickle, not the flood) and the values it returns are still correct. Every named caching problem is a specific way one of those invariants breaks — the miss path suddenly re-exposes the origin, one key distorts the node it lives on, or the cached copy stops matching the truth. Naming them matters because each has a different fix, and the fixes trade against one another.

The diagram below places each failure at the exact point in the client → cache → origin pipeline where it strikes; the rest of the page then traces one of them (penetration) end to end with real numbers.

diagram
diagram

The catalogue

Numbers 1 and 5 in the older lists — "thundering herd" and "cache stampede (dogpile)" — are the same phenomenon under three names: a hot key expires and a burst of concurrent misses hits the origin with identical queries at the same instant. This page counts it once; its mechanism and the four real fixes (single-flight, lock + serve-stale, probabilistic early expiry, stale-while-revalidate) are traced in Cache Stampede & Invalidation (page 011).

ProblemInvariant it breaksMechanism (how it hurts)Primary fix
Stampede / thundering herd / dogpileHit rate (origin protection)Hot key expires → many concurrent misses issue the same origin query at onceCoalesce misses; refresh before expiry (see 011)
PenetrationHit rate (origin protection)Requests target keys that never exist, so the cache can never hold them and every read reaches the originNegative-cache the miss; bloom-filter the key space
Hot keyHit rate (per-node)One key takes a large share of traffic; hashing pins it to a single node that then saturatesReplicate the key; add a client-local L1
Big keyHit rate (per-node)One value is large enough to dominate a node's memory and block it on serialize / deleteSplit into chunks; store blobs elsewhere
PollutionHit rate (working set)A flood of one-off keys evicts the small, frequently-read working setLFU / admission policy (TinyLFU)
DriftCorrectnessOrigin data changes but the cached copy is not invalidated, so reads return stale valuesInvalidate / version on write (see 005, 011)

Three of these carry a subtlety worth spelling out before we trace one in full.

Hot key — why the textbook fix is wrong

Naive claim (incorrect): "use consistent hashing to spread a hot key's load across nodes." Consistent hashing maps one key to exactly one node; a single hot key therefore lands entirely on one node no matter how many nodes you add. Consistent hashing spreads keys, not the traffic of one key. What actually works: replicate the value under N suffixed keys (trending#0 … trending#3) and have each client read a random replica, spreading a 4× share across four nodes; and/or put a tiny in-process L1 cache in front of the shared cache so most hot-key reads never leave the app server. To find the offending key before it topples a node, sample the traffic: redis-cli --hotkeys (uses the LFU counters) or your cache proxy's per-key request stats will surface the one key eating a disproportionate share.

Big key — the blocking cost, not just the memory

A 50 MB value is not merely 50 MB of capacity. On Redis a single-threaded command against it — DEL, or serializing it onto the wire — blocks the whole node for the duration, stalling every other key on that shard. Use UNLINK (async free) instead of DEL, and split the value into bounded chunks or move large blobs to object storage with only a pointer in the cache.

Pollution — a scan poisons the working set

The classic trigger is a batch job or crawler that reads millions of keys once. Under plain LRU, each cold key is "most recently used" the moment it lands, so it evicts a genuinely hot key. A frequency-aware policy (LFU, or an admission filter like TinyLFU that refuses to admit a brand-new key over a proven-hot one) resists this; see page 010 for how each policy decides.

Worked example — cache penetration, with real numbers

A product API serves 50,000 rps at a 98% hit rate, so the database sees 50,000 × 2% = 1,000 rps — exactly its comfortable budget. An attacker (or a broken client) now sends 20,000 rps for product IDs that do not exist (id=-1, random 18-digit IDs). The standard handler reads: on miss, query DB; if the row is null, return 404 and cache nothing. Trace what the DB sees:

#RequestCache lookupHandler actionDB queries this step
1id=-1missDB returns null → not cached → 4041
2id=-1 (again)miss (still absent)DB returns null again → 4041
3id=99999999missDB returns null → 4041
20k distinct/repeated bad IDsalways missevery one reaches the DB≈ 20,000 rps

Legitimate traffic still needs its 1,000 rps, so the DB is asked for ~21,000 rps against a 1,000 rps budget — 20× over, and it falls over, taking real users down with it. The cache did nothing, because you cannot cache the absence of a row you never store.

Fix A — negative caching: cache the null result itself, e.g. SET product:-1 "__MISS__" EX 30. Repeated bad IDs now hit the cache; if the attacker reuses a small set of IDs, DB load collapses to (distinct bad IDs ÷ 30 s). Fix B — bloom filter: keep a bloom filter of all valid product IDs in memory; a lookup that the filter reports as "definitely absent" is rejected before any cache or DB call, so even high-cardinality random IDs cost zero DB queries.

diagram
diagram

Pitfalls

Choosing the mitigation — and its cost

The problems are a catalogue; the engineering is picking the right guard for your traffic shape, because each guard buys protection with a specific cost.

Penetration: negative cache vs bloom filter

Signal: misses for keys that resolve to nothing. Low-cardinality abuse (a handful of bad IDs, retried) → negative caching: trivial to add, but it consumes cache space for junk and risks stale 404s. High-cardinality abuse (random IDs, near-infinite distinct values) → bloom filter: O(1) memory in bits per key and rejects before any lookup, but it costs a maintained side-structure, tolerates rare false positives, and is awkward to update. Choose the bloom filter when the bad key space is unbounded; prefer negative caching when the bad keys repeat and simplicity matters.

Hot key: key replication vs client-local L1

Replication spreads a key's reads across N nodes but adds write fan-out and a cross-replica consistency window. A client-local L1 (a small in-process cache) removes the hot-key reads from the network entirely — cheapest for reads — but each app instance can serve a slightly stale copy for its short local TTL. Choose L1 when a few seconds of staleness is fine and you have many app servers; prefer replication when reads must be near-fresh but one node can't carry the volume.

Pollution: LFU/TinyLFU vs plain LRU

LRU is cheap and great for recency-dominated traffic, but a one-pass scan poisons it. LFU / TinyLFU protects a stable working set against scans at the cost of more per-entry bookkeeping and slower adaptation when the working set genuinely shifts. Choose frequency-aware policies when a known hot set coexists with bursty cold scans; stay on LRU when access is dominated by recency and simplicity wins.

Takeaways


Re-authored and deepened for this guide. Draws on Alex Xu, System Design Interview (Vol. 1–2, cache design and stampede protection); Nishtala et al., Scaling Memcache at Facebook (NSDI 2013) for leases, hot-key replication, and thundering-herd control; the Redis documentation on maxmemory-policy (LRU/LFU), big-key handling, and UNLINK; and Einziger et al., TinyLFU: A Highly Efficient Cache Admission Policy. The stampede/thundering-herd mechanism is treated fully in the companion page "Cache Stampede & Invalidation."

🤖 Don't fully get this? Learn it with Claude

Stuck on Caching Challenges? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Caching Challenges** (System Design) and want to truly understand it. Explain Caching Challenges from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Caching Challenges** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Caching Challenges** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Caching Challenges** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes