Back to the map

Module 06 · Caching · Lesson 4 of 8

Cache invalidation

Stale data is the price of caching. TTLs, explicit deletes, versioned keys and event-driven purges each bound staleness differently, and the races between reads and writes are where real bugs live.

DeepMust-knowMid 16 minPro Updated Nov 2026

Why it matters

A cache that is never updated is a bug generator. A user changes their email and the old one keeps appearing. A price drops and the cached page still shows the old number for an hour. Invalidation is how the cache learns that the world changed, and it is the part of caching most teams get wrong, because the hard cases are races that only show up under load.

The idea

There are four ways to stop serving a stale value, and most systems use two or three together.

Expire it (TTL). Every key carries a lifetime. Simple, requires no coordination with writers, and bounds staleness to the TTL. The cost is that the bound is the whole TTL: a five-minute TTL means up to five minutes of wrong answers after every write.

Delete it on write. When the application writes to the database, it also deletes (or updates) the cache key. Staleness drops to the gap between the two operations. Requires every write path to know every key it affects, which is why it drifts out of sync as a codebase grows.

Change the key. Instead of invalidating user:7, read user:7:v12 where the version is stored with the record or derived from an updated-at timestamp. A write bumps the version; old entries become unreachable and expire on their own. No deletes, no races, at the cost of one extra lookup to learn the current version.

React to the change log. Capture database changes (CDCChange Data Capture: streaming the database's own commit log so other systems can react to each insert, update or delete.) and publish invalidations from the stream. Writers do not need to know about the cache at all, and every path that changes the data, including migrations and admin tools, triggers the purge. Adds a pipeline and a short propagation delay.

Writer1 UPDATEDatabase2 DEL keyCacheReader3 miss4 read fresh, refill
Delete-on-write: the write path updates the database, then removes the cache key so the next read refills it.

The race nobody draws

Delete-on-write has a hole. Reader A misses and reads the old row from the database. Writer B updates the row and deletes the cache key. Reader A, slow, now writes its old value into the cache. The cache holds stale data until the TTL, even though every step was "correct".

Three mitigations show up in practice:

  • Delete after the write, and again after a delay ("double delete"): the second delete, a few hundred milliseconds later, clears any value a slow reader filled in between.
  • Leases: on a miss the cache hands the reader a token; the reader's fill is accepted only if no invalidation happened since the token was issued. Facebook's Memcached paper describes this; it also collapses concurrent misses.
  • Short TTL as a backstop: whatever else you do, keep a TTL so the worst case is bounded. Correctness through expiry is dull and reliable.
 TTL onlyDelete on writeVersioned keysCDC-driven purge
Staleness boundFull TTLMilliseconds, except the raceMillisecondsPipeline lag, typically under a second
Writer must know cache keysNoYes, every write pathOnly bumps a versionNo
Race-proneNoYesNoNo
Extra costNoneOne delete per writeOne version lookup per readA CDC pipeline
Catches out-of-band writesEventuallyNoOnly if they bump the versionYes
Cache invalidation · Backplane