System Design · Module 6 · Lesson 3 of 4 · 40 min
Invalidation Races and Freshness Guarantees
Reproduce a stale-fill race, define how quickly updates become visible, and choose a stronger policy when TTL staleness is unacceptable.
01 The idea
The problem this lesson solves
Reader R misses and obtains v1 from the database.
Prerequisite: earlier lessons introduce the vocabulary and shared examples used here. Follow the module order.
02 Worked example
Follow one worked example
Trace the example in order. Follow the same identities, units and state transitions through the diagram and the events below.
Deleting a cached value after a write does not prevent an overlapping old reader from filling it again.
03 The method
Mechanisms decisions and a guided tutorial
Work through the explanation in order, then apply it in the tutorial. Each comparison names a cost as well as a benefit.
| Choice or concept | Purpose | Boundary or cost |
|---|---|---|
| Plain cache-aside | Simple fast path | Overlapping stale fills remain possible |
| TTL | Bounds copy lifetime | Does not eliminate the race |
| Version-aware coordination | Rejects obsolete work under its contract | Needs authoritative ordering/progress |
| Bypass sensitive reads | Uses the stronger authority directly | Higher origin cost |
Walk the invalidation race
A reader misses and reads old destination v1 from the database. A writer then commits v2 and deletes the cache entry. The slow reader finally stores v1 into the now-empty cache. ‘Update database, then delete cache’ did not prevent this stale fill. A TTL bounds how long that fill normally survives, but does not eliminate the race. Stronger freshness can require version checks, coordinated fills, invalidation events with suitable ordering, or bypassing the cache for sensitive reads.
Trade freshness for bounded latency explicitly
Stale-while-revalidate serves a bounded stale value while one worker refreshes it. This can work for a public product description whose contract allows brief staleness. It is usually inappropriate for a security revocation check unless the security policy explicitly accepts that delay. Negative caching stores ‘not found’ briefly to reduce repeated misses; creating the previously absent object must invalidate or outlive that negative entry. Return privacy-safe errors regardless of whether the result was cached.
Walk the race event by event
At t1, reader R misses and reads destination v1. At t2, writer W commits v2. At t3, W deletes the cache. At t4, R finishes late and stores v1. The cache now contains v1 even though the writer followed “database first, delete cache second.”
A TTL limits how long this particular cached copy normally survives, but it did not prevent the race. Adding an arbitrary sleep before another delete is not a proof against all delays. State the actual freshness contract before deciding whether bounded staleness is acceptable.
Define the acknowledgment boundary
For a public description, “updates appear within sixty seconds” may be a deliberate product contract. For a revoked permission or changed safety-critical setting, a stale positive decision can be unacceptable. Define whether the guarantee applies to reads starting after update acknowledgment and what happens to reads already in flight.
Stronger designs may bypass cache for sensitive decisions, coordinate version-aware fills, or use an invalidation/update stream with an explicitly reasoned ordering and recovery policy. Merely placing a version number in the value does not help if a stale fill is accepted without comparing it against authoritative progress.
Make negative and stale serving policies explicit
A short-lived negative result can shield the origin from repeated requests for a nonexistent code, but a newly created code may remain hidden until the negative entry is invalidated or expires. An origin timeout should not become a negative cache entry because it did not establish absence.
Stale-while-revalidate deliberately serves an older copy during a bounded grace period while one refresh occurs. It can improve latency for eligible public content. It must not be silently applied to a contract requiring immediate revocation. Freshness is a business guarantee expressed through a serving policy, not a synonym for speed.
Guided tutorial
1. Draw two lanes
Put reader and writer events on a shared timeline.
2. Insert delays
Pause R after the database read and before the cache fill.
3. Write a guarantee
Specify the allowed staleness and update acknowledgment boundary.
4. Choose a policy
Compare bounded TTL with bypass for mutable security-sensitive decisions.
R: cache miss → database returns v1 → pause
W: commit v2 → delete cache → acknowledge update
R: resume → cache.set(v1, ttl)
# Result: stale v1 exists after the delete.
# A stronger contract must reject this fill or avoid this read path.04 Cheat sheet
Review the decisions
Use this table to explain the mechanism and its limit without memorizing a product name.
| Choice or concept | Remember | Check |
|---|---|---|
| Plain cache-aside | Simple fast path | Overlapping stale fills remain possible |
| TTL | Bounds copy lifetime | Does not eliminate the race |
| Version-aware coordination | Rejects obsolete work under its contract | Needs authoritative ordering/progress |
| Bypass sensitive reads | Uses the stronger authority directly | Higher origin cost |
05 Where & why
Check the primary references
Use official references for protocol and implementation details. The questions here are teaching prompts.
06 Interview questions
Explain the reasoning out loud
Answer before opening the explanation. The questions progress from vocabulary to decisions and failure behavior.
What does Invalidation mean?
What does Stale fill mean?
What does Version check mean?
Why is delete-after-write insufficient?
Does a version field alone solve the race?
When is bounded staleness acceptable?
What must a revocation guarantee specify?
How can negative caching hide a new object?
07 Practice problems
Practice a new case
Solve the new cases before revealing hints. Complete worked solutions follow the set.
Hit rate
EasyShow the hint
Name the race
EasyShow the hint
Staleness bound
MediumShow the hint
Negative create
MediumShow the hint
Version proof
MediumShow the hint
Correct redirect update
HardShow the hint
Worked solutions
1. Hit rate
18 / (18 + 6) = 0.75, or 75 percent.
2. Name the race
The late fill writes old v1 after the newer commit and invalidation.
3. Staleness bound
It normally expires at t=50, not thirty seconds after the writer’s update. Other stale-serving or refresh policies can change the full contract.
4. Negative create
Readers can see not found for the remaining negative lifetime unless creation invalidates it or another freshness mechanism intervenes.
5. Version proof
The claim is incomplete. The fill must compare against trustworthy newer state under a coordinated protocol; storing a timestamp without enforcing ordering does not reject a stale write.
6. Correct redirect update
Do not promise immediate freshness with plain cache-aside and TTL. Read through a coordinated version-aware mechanism that rejects stale fills and defines concurrent-read ordering, or bypass the cache for these mutable links. State the acknowledgment boundary and handle already-in-flight reads explicitly.