Invalidation Races and Freshness Guarantees

Caching and Failure Recovery · 40 min

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.

Deleting a cached value after a write does not prevent an overlapping old reader from filling it again.

01 The idea

The problem this lesson solves

You update the database and delete the cache key. A moment later the cache contains the old value again. No second writer changed the database. An earlier reader finished late and refilled stale data after your deletion. Cache invalidation is a concurrency problem: the order of reads, writes and fills matters as much as the command used to remove a key.

You should recognize the application, the database and the request path. Treat the database as the authority unless explicitly stated otherwise.

InvalidationRemoving or marking a cached copy unusable after a relevant change.
Stale fillA delayed reader populating an old value after a newer source update.
Version checkComparing an update or fill against a version to reject obsolete work under a coordinated policy.

02 Worked example

Refill an old value after invalidation

Invalidation Races and Freshness Guarantees worked diagram01R readsv1 from database02W writesCommit v2 + delete03R fills lateCaches v1 again04Freshness policyBound or reject stalefillRead events in time order, from left to right.Invalidation Races and Freshness Guarantees compact diagram01R readsv1 from database02W writesCommit v2 + delete03R fills lateCaches v1 again04Freshness policyBound or reject stale fill
Invalidating after the write still allows an older in-flight read to refill v1. Choose a freshness policy that handles this race.
1
R loads old stateReader R misses and obtains v1 from the database.
2
W commits new stateWriter W stores v2 and invalidates the cached key.
3
R fills lateThe older reader stores v1 after the invalidation.
4
Evaluate the contractTTL permits bounded stale serving; immediate visibility requires stronger coordination or bypass.

03 The method

Understand the mechanism

Define the freshness promise per operation

Freshness describes how current a returned value must be relative to authoritative changes. A public content page may tolerate a short stale interval; an access revocation or ownership decision may not. Start with the user-visible contract rather than a default TTL.

Read-your-writes means a client can observe its own completed update on a later read under the stated session or operation scope. It is weaker than promising that every reader everywhere immediately observes the newest value. Monotonic reads prevent a client from going backward to an older observed version.

A cache can violate these expectations even when the database itself provides suitable guarantees. If the app reads an old local copy after a successful write, the user experiences stale behavior despite the authoritative row being correct.

Separate strict operations from tolerant ones where the product allows it. An owner’s confirmation path might read the authority, while anonymous browsing uses an explicitly bounded-staleness path. The distinction must be real in the implementation and documentation.

Walk the stale-fill race

Initially the database holds v1 and the cache is empty. Reader R misses and reads v1 from the database, then pauses before filling the cache. Writer W commits v2, deletes the cache key and reports success. R resumes and stores v1 with a fresh TTL.

The cache now contains old data after the writer’s invalidation. The delete command worked exactly as requested; it could not delete a fill that had not happened yet. “Delete after write” is therefore not by itself proof of immediate freshness.

If the writer instead deletes before committing, another reader can miss during the gap, read v1 and refill it before or after the commit. Changing the order alone does not eliminate every interleaving.

Draw the race with separate database and cache columns. This makes the authority and copy transitions visible. A single arrow labeled update cache hides the interval where the problem occurs.

Understand what TTL can and cannot bound

A TTL begins according to the cache insertion or refresh operation. If R read v1 long ago and fills it after W’s update, its new TTL starts late. The maximum age relative to the latest database change is not necessarily equal to the configured TTL.

To claim a bound, account for the age of the source read, maximum fill delay, expiry behavior and any upstream caches. An unbounded delayed fill destroys a simple “all readers fresh within thirty seconds of update” argument.

TTL remains useful: it limits residence of a particular entry and provides eventual refresh opportunities under ordinary operation. It is a repair mechanism for some stale-copy situations, not a substitute for a strict consistency protocol.

Be careful with stale-while-revalidate and stale-if-error policies. They intentionally permit serving older data under defined conditions. They can improve availability for suitable content, but they weaken freshness and must not be applied blindly to permissions or revoked destinations.

Compare invalidation, updates and versioned fills

Invalidation removes a copy and lets a later read refill it. Updating the cached value can reduce the next miss, but concurrent writers and delayed fills still need ordering. A slower writer or reader can overwrite a newer cache value unless the cache transition rejects older versions.

Versioned values can help if the compare-and-store is atomic and there is trustworthy version knowledge to compare against. Merely placing version=1 beside a value is insufficient when the cache is empty and accepts any incoming fill.

A design may maintain a version watermark, use generation-specific keys, or validate against an authoritative version before accepting a fill. Each adds state and ordering requirements. If validation and fill are separate and uncoordinated, another update can occur in between.

For a strict read path, bypassing the cache may be the clearest first choice, provided the authoritative read itself meets the required consistency. The performance cost is explicit and can be measured. Stronger cache coordination should be introduced only with a complete failure and race argument.

Keep multiple layers and messages in the model

A local app cache and a shared cache are two copies. Deleting the shared entry does not remove A’s local entry unless another mechanism connects them. Browser or CDN caching can retain an HTTP response independently of both.

Invalidation messages can arrive late, out of order, more than once or not at all under the transport and recovery model. Consumers need appropriate version/order handling and a way to recover after disconnection. A message bus labeled reliable does not eliminate the need to define these behaviors.

Reordering matters: an old update message must not replace a newer value. Duplicate invalidation is often harmless, while duplicate write application may not be. The event’s semantics determine what idempotency means.

Include tenant, representation and query identity in invalidation. Removing a7’s object key may not invalidate cached owner listings or derived search results that contain its old destination. Those projections need their own consistency contracts.

Test freshness with controlled interleavings

Ordinary sequential tests rarely expose late-fill races. Add a barrier in a practice loader after the authoritative read and before the cache write. Pause R at that barrier, perform W’s update and invalidation, then resume R. Inspect the cached value.

Repeat with multiple writers, delayed invalidation, local and shared caches, and a restarted invalidation consumer. Check the declared contract rather than assuming every stale observation is equally wrong. A permitted thirty-second-old public value differs from a stale authorization decision.

Observe value versions and fill timing where feasible without exposing sensitive content. A high hit ratio does not indicate freshness. Metrics need to reflect invalidation failures, rejected stale fills and divergence checks when those mechanisms are used.

The design is complete when it explains both the normal fast path and why an old operation cannot violate the promised read behavior. If the mechanism only reduces the probability of stale data, describe it as risk reduction rather than a guarantee.

Guided tutorial

  1. Reproduce the exact race

    Use three states on paper: database=v1, cache=missing, R holds no value. Let R read v1 and pause. Let W commit v2 and delete the key. Resume R’s fill with v1. The final database and cache disagree even though deletion occurred after the write.

  2. Calculate the TTL limitation

    Assume R pauses for twenty seconds after reading and then fills v1 with a thirty-second TTL after W’s update. Identify the extra delay introduced by the late fill. Explain why TTL alone is not a thirty-second bound measured from the authoritative update.

  3. Review a proposed version check

    Store versions with values, then consider an empty cache that has forgotten the newest version. Can it reject an old v1 fill? Not without another source of version knowledge. Add a watermark or choose an authoritative bypass and explain the atomic boundary required.

  4. Write two read contracts

    Define a tolerant public read and a strict post-update verification read. Draw the path each uses, including local, shared and HTTP caches. Test the delayed-reader timeline against both and state exactly which stale result is permitted.

Concurrent event trace

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.

Check your result

Pause R after reading v1, commit v2 and delete the key in W, then resume R. The cache contains stale v1. Explain how your chosen freshness contract handles or prevents that fill.

Reproduce a late stale fill

Explicit variables model the paused reader without threads or timing luck. The interleaving is the defect; a delete command alone cannot prevent it.

database = {"a7": (1, "old")}
cache = {}
reader_value = database["a7"]  # R reads v1, then pauses.
database["a7"] = (2, "new")    # W commits v2.
cache.pop("a7", None)          # W invalidates.
cache["a7"] = reader_value     # R resumes its old fill.
print("database_version=", database["a7"][0], sep="")
print("cache_version=", cache["a7"][0], sep="")
print("stale=", cache["a7"][0] < database["a7"][0], sep="")

Expected output

database_version=2
cache_version=1
stale=True

The cache fill occurs after the delete and still contains v1. A strict fix needs an authority/version protocol or a different read path, not merely the same commands with optimistic comments.

04 Cheat sheet

Review the decisions

Use this table to explain the mechanism and its limit without memorizing a product name.

Choice or conceptRememberCheck
Plain cache-asideSimple fast pathOverlapping stale fills remain possible
TTLBounds copy lifetimeDoes not eliminate the race
Version-aware coordinationRejects obsolete work under its contractNeeds authoritative ordering/progress
Bypass sensitive readsUses the stronger authority directlyHigher origin cost

05 Where & why

Check the primary references

Use official references for protocol and implementation details. The questions here are teaching prompts.

Primary reference
HTTP caching semantics

Read HTTP caching semantics

Primary reference
Redis TTL semantics

Read Redis TTL semantics

Primary reference
Redis atomic server-side scripting

Read Redis atomic server-side scripting

State the implementation version and the workload assumptions when defending your own design.

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?
Removing or marking a cached copy unusable after a relevant change.
What does Stale fill mean?
A delayed reader populating an old value after a newer source update.
What does Version check mean?
Comparing an update or fill against a version to reject obsolete work under a coordinated policy.
Why is delete-after-write insufficient?
A reader can fetch old data before the commit and fill it after the delete. The timeline contains no step that rejects that late work.
Does a version field alone solve the race?
No. The fill path must compare against trusted newer progress and reject obsolete data. An unchecked label changes no coordination behavior.
When is bounded staleness acceptable?
When the product explicitly permits it for that operation and the failure behavior stays within the contract. Public descriptions and security revocations can have very different policies.
What must a revocation guarantee specify?
Its acknowledgment boundary, eligible reads, in-flight behavior and recovery path. “Immediately” is ambiguous without those details.
How can negative caching hide a new object?
A retained not-found result can outlive the source’s absence. Creation should invalidate it or the contract must accept its bounded lifetime.

07 Practice problems

Practice a new case

Solve the new cases before revealing hints. Complete worked solutions follow the set.

Hit rate

Easy
Compute the hit rate for 18 hits and 6 misses.
Follow-up
Use total lookups as the denominator.
Show the hint
Hits plus misses equals the total.

Name the race

Easy
Identify the stale operation in read v1, commit v2, delete, fill v1.
Show the hint
The last event uses an earlier observation.

Staleness bound

Medium
A stale fill occurs at t=20 with TTL 30 seconds; state its normal expiry time under the simple model.
Show the hint
TTL starts from insertion, not the earlier source read.

Negative create

Medium
A key is negatively cached for a minute, then created ten seconds later; explain the visibility risk.
Show the hint
The cached absence remains a copy too.

Version proof

Medium
Review “we put updated_at in the cached JSON, so stale fills are impossible.”
Show the hint
Ask which atomic check rejects older work.

Correct redirect update

Hard
Design a cache policy for links whose destination changes must be visible immediately after update acknowledgment.
Follow-up
A reader may have fetched an old value before the update.
Show the hint
A TTL only bounds staleness; it does not prevent the late fill.

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.