The cache was working beautifully at 94% hit rate. Then a popular product's entry expired at 3pm on a Friday, and forty thousand requests that had all been served from memory a second earlier arrived at the database simultaneously. The database did what databases do in that situation, which is stop. The postmortem called it a cache stampede; the on-call engineer called it several other things.
Caching interview questions are rarely about what a cache is. They are about the failure modes — stampedes, stale reads, invalidation, hot keys — because a cache is the component most likely to turn a slow system into a broken one. This guide covers the patterns, the eviction policies, invalidation, and the questions interviewers actually ask.
What caching interviews actually test
- The patterns — cache-aside, read-through, write-through, write-behind, and when each fits.
- Invalidation — the genuinely hard part, and how you avoid stale reads.
- Failure modes — stampede, penetration, avalanche, hot keys.
- Eviction and sizing — LRU, LFU, TTL strategy, memory pressure.
- Placement — browser, CDN, application, database — and what belongs where.
The caching patterns
- Cache-aside (lazy loading) — the application checks the cache, and on a miss reads the database and populates the cache itself. The default, used almost everywhere. Only requested data is cached, and the cache can be down without breaking reads. Downside: every miss pays the full latency, and there is a window where the cache and database disagree.
- Read-through — the same shape, but the cache library fetches on a miss rather than your code. Cleaner application code, less control.
- Write-through — writes go to the cache and the database synchronously. The cache is never stale, at the cost of write latency, and you cache data that may never be read.
- Write-behind (write-back) — writes go to the cache and are flushed to the database asynchronously. Very fast writes, and you can lose data if the cache dies before flushing. Only acceptable where loss is tolerable.
- Refresh-ahead — proactively refresh entries before they expire. Helps for predictable hot keys, wasteful otherwise.
The interview-worthy summary: cache-aside plus a TTL is the sensible default, and you move to write-through only when stale reads are genuinely unacceptable. Candidates who reach for write-behind without mentioning durability get pushed on it.
Invalidation, and why it is hard
Expect a version of "how do you keep the cache consistent with the database?" The honest answer is that you generally do not achieve strict consistency — you bound staleness. The options:
- TTL — the workhorse. Simple, self-healing, and it bounds staleness to the TTL. Most systems need nothing more.
- Explicit invalidation on write — delete the key when the underlying row changes. Better freshness, and it introduces a race: if you delete then write, a concurrent read can repopulate the cache with the old value before the write lands.
- Update-on-write instead of delete — tempting, but two concurrent writes can land in the cache in the opposite order from the database. Deleting is safer than updating, and saying that is a strong signal.
- Versioned keys — include a version or updated-at in the key so old entries become unreachable and expire naturally. Avoids the race entirely at the cost of key churn.
- Change data capture — invalidate from the database's replication stream, which catches writes that bypass your application.
The nuance worth voicing: you cannot atomically update a cache and a database — they are two systems without a shared transaction. So every design has a window, and the question is how small and how harmful it is. That framing is what separates a senior answer.
The failure modes
This is where the round is decided.
Cache stampede (thundering herd) — the story in the intro. A hot key expires and every concurrent request misses simultaneously, all hitting the database. The fixes:
# Single-flight: only one caller recomputes; the rest wait for that result
def get_product(product_id):
key = f"product:{product_id}"
cached = redis.get(key)
if cached is not None:
return deserialize(cached)
# NX lock — only the first caller wins and does the DB read
lock = redis.set(f"lock:{key}", "1", nx=True, ex=5)
if not lock:
time.sleep(0.05) # brief wait, then re-read
return get_product(product_id)
try:
value = db.fetch_product(product_id)
# Jittered TTL so 40k keys do not expire in the same second
ttl = 300 + random.randint(0, 60)
redis.setex(key, ttl, serialize(value))
return value
finally:
redis.delete(f"lock:{key}")
Name all three mitigations: a lock or single-flight so one caller recomputes, jittered TTLs so keys do not expire together, and probabilistic early expiry where a request near expiry refreshes proactively while still serving the cached value. Also mention serving stale data while refreshing in the background, which is often the best user experience.
The others:
- Cache penetration — requests for keys that do not exist in the database either, so nothing is ever cached and every request hits the database. Common in enumeration attacks. Fix by caching the negative result with a short TTL, or a Bloom filter for existence checks.
- Cache avalanche — a large fraction of the cache expires or the cache node dies, sending everything to the database at once. Fix with jittered TTLs, replicas, and a circuit breaker or rate limiter protecting the database.
- Hot keys — one key gets a disproportionate share of traffic and saturates a single cache node. Fix by replicating that key across nodes with a suffix, or adding a local in-process cache in front of the shared one.
Eviction, TTLs and sizing
Know the policies and the reasoning: LRU evicts the least recently used and suits general workloads with temporal locality; LFU evicts the least frequently used and suits stable popularity but adapts badly to changing trends; FIFO is simple and usually worse; TTL-based expiry is orthogonal and combines with any of them. Redis exposes several maxmemory-policy options including allkeys-lru and volatile-lru, and knowing that distinction is a common check.
On sizing, the honest answer is that hit rate is the metric that matters, and there are diminishing returns — going from 90% to 95% halves the database load, but 95% to 96% barely moves it. Measure hit rate, miss latency and eviction rate; a high eviction rate means the working set does not fit.
On TTLs, tie the value to how stale the data may safely be, not to a round number. A product price might tolerate 30 seconds; a user's display name might tolerate an hour. Our Redis interview questions guide covers the implementation layer.
Where to cache
- Browser —
Cache-Control,ETag, immutable hashed asset filenames. Free and the fastest possible cache. - CDN — static assets and cacheable API responses at the edge, near the user.
- Application in-process — nanosecond access, but per-instance, so it is inconsistent across servers and multiplies memory. Good for small, rarely-changing reference data.
- Distributed cache (Redis, Memcached) — shared across instances, the main layer people mean. Adds a network hop and a dependency.
- Database — query cache, buffer pool, materialised views.
A good design answer walks the layers and says what belongs where, then adds the discipline point: every cache layer is another place data can be stale, and a four-layer stack means four invalidation problems. Our system design interview guide covers where this sits in a full design.
The papers, real systems, ChatGPT — where each fits
- Designing Data-Intensive Applications — the reasoning about consistency windows that underlies every invalidation answer.
- The Redis documentation on eviction and keyspace notifications — concrete, short and directly interview-relevant.
- Facebook's memcached paper ("Scaling Memcache at Facebook") — the source of the lease and stampede mitigations, and a genuinely good read.
- Building one and breaking it — highest yield. Put a cache in front of a database, set every key to the same TTL, and watch the periodic latency spike. You will never forget to jitter again.
- Your own metrics — hit rate, eviction rate and p99 miss latency, because the interview question "how would you know it is working?" wants those names.
- ChatGPT — good for explaining patterns and generating cache code. It will not ask what happens when that one key expires on a Friday afternoon.
- Greenroom — the spoken layer. Ari, the AI interviewer, runs design rounds out loud and pushes the failure-mode follow-up. Fair tradeoff: Ari will not tune your Redis instance.
How to prepare for a caching interview
- Week 1: patterns. Implement cache-aside and write-through, and articulate the staleness and durability tradeoff of each aloud.
- Week 2: invalidation. Build the delete-then-write race deliberately, then fix it with versioned keys, and explain why deleting beats updating.
- Week 3: failure modes. Cause a stampede with uniform TTLs, then fix it with a lock and jitter; add negative caching for penetration.
- Final week: placement and metrics — walk the full layer stack aloud for a real product, then run two spoken mocks with design follow-ups.
Building the rest of the stack? The Redis guide covers the cache itself, the database sharding guide covers scaling the layer behind it, the load balancing guide covers the layer in front, and the system design guide covers assembling them.
Frequently asked questions
What are the most common caching interview questions?
The most common questions cover the difference between cache-aside, read-through, write-through and write-behind and when each is appropriate, how you keep a cache consistent with the database, what a cache stampede is and how you prevent it, the difference between LRU and LFU eviction, how you handle hot keys, and where caching belongs across browser, CDN, application and database layers.
What is the difference between cache-aside and write-through caching?
With cache-aside the application checks the cache, and on a miss reads the database and populates the cache itself, so only requested data is cached and the system still serves reads if the cache is unavailable. With write-through, writes go to both the cache and the database synchronously, so the cache is never stale but write latency increases and you cache data that may never be read. Cache-aside plus a TTL is the sensible default.
What is a cache stampede and how do you prevent it?
A cache stampede happens when a popular key expires and many concurrent requests all miss simultaneously, sending a flood of identical queries to the database. Prevent it three ways: use a lock or single-flight so only one caller recomputes while others wait, jitter TTLs so many keys do not expire in the same second, and use probabilistic early expiry where a request approaching expiry refreshes proactively while still serving the cached value.
Why is cache invalidation considered hard?
Because you cannot atomically update a cache and a database — they are separate systems with no shared transaction, so every design leaves a window where they disagree. Explicit invalidation introduces races, such as a concurrent read repopulating the cache with a stale value between a delete and a write. Deleting a key is safer than updating it, because two concurrent updates can reach the cache in the opposite order from the database.
What is the difference between LRU and LFU eviction?
LRU evicts the least recently used entry, which suits workloads with temporal locality where recently accessed data is likely to be accessed again. LFU evicts the least frequently used entry, which suits stable popularity distributions but adapts poorly when trends change, since a formerly popular item can occupy memory long after interest has moved. Redis exposes both through its maxmemory-policy setting, in allkeys and volatile variants.
How do you handle a hot key in a distributed cache?
A hot key concentrates traffic on the single node that owns it, saturating that node while others idle. The standard fixes are replicating the key across several nodes with a suffix and having clients pick one at random, or adding a small in-process local cache in front of the shared cache so most reads never leave the application server. Both trade a little extra staleness for a large reduction in load concentration.