Cache Invalidation Strategies: Purges, Versioning, and Write-Through Choices
Cache invalidation strategies compared: delete-on-write, versioned cache keys, the cache purge, and write-through vs write-behind (plus the delivery problem and the resurrection race) part 6 of the Caching in System Design series.
Cache invalidation is the act of removing or refreshing cached copies when the data they copy changes, so that readers stop receiving stale data before any TTL would have stopped them on its own. Where expiry is passive (the cache forgets on a schedule) invalidation is active: the system remembers what changed and takes responsibility for the copies. That responsibility is the whole difficulty, and it has three parts: knowing what changed, knowing who holds copies of it, and getting the removal to those holders at the right moment.
Why invalidation is hard: what changed, who holds copies, and when
The first question is what changed, and it is harder than it sounds because caches do not store rows, they store shapes derived from rows. One product update fans out to a product page, a category listing, a search result, a personalized feed, each cached under its own key. The write path knows the row; the cache knows the shapes; and the mapping between them lives nowhere unless the design puts it somewhere. This fan-out is the real source of invalidation’s reputation: naming every copy a change touches is a derivation problem, not a deletion problem, and the techniques in this article are different ways of living with a mapping nobody wrote down.
The second question is who holds copies, part 2’s territory: the owning node in a fleet, the tier-one caches on every application instance, sometimes the client. The third is when: the removal has to land after the commit and before the next read of the changed data. Too early, and it races the transaction; the ordering rule part 4 stated: invalidate after the commit, in the code path that knows it succeeded. Too late, and the system is serving data it knows is stale.
Across one process, invalidation is a function call. Across a fleet it is message delivery, and part 2 priced the three ways it can be done: broadcast the delete everywhere, gossip until the fleet converges, or cap divergence with TTLs and accept the window. Delivery in practice is at-most-once; a pub/sub message is not retried, part 5’s keyspace notifications can be missed, and a message lost is a stale copy that survives until its TTL. That is why the rule part 4 stated (every key carries an expiry) is not a caching habit but the correctness backstop under every immediate technique in this article: the TTL is the invalidation you get when the invalidation you wanted never arrives.
The honest engineering answer to the fan-out problem is to write the mapping down, and there are only three places it can live. The derivation ledger: the data layer declares which namespaces derive from which tables, so the write path can name its targets instead of guessing them. The shared key registry: every cached shape is registered at construction, and the invalidation code reads the registry rather than a developer’s memory. And the escape hatch for shapes nobody can enumerate; move them to versioned keys, the technique of the next sections, and let the naming do the tracking. What does not work is the default: invalidation paths discovered during incidents, one stale shape at a time.
The taxonomy: four deliberate techniques
Part 2 promised this catalog in three words (purges, versioned keys, write-through) and part 1’s dial deserves the fourth added: delete-on-write, the workhorse the other three are measured against. One line each:
- Delete-on-write. After the commit, remove the derived key; the next read misses and repopulates fresh. The default, and the baseline every alternative is measured against.
- Versioned keys. Change the key’s name (a version prefix or an entity version) so old copies become unreachable garbage that ages out by TTL. Invalidation by renaming, with no delivery to lose.
- Purge. Bulk removal of a namespace or pattern, for changes whose fan-out is too broad or too unknown to enumerate key by key.
- Write-through. Update the copy on the write path instead of removing it; the key never goes cold. The only technique that keeps the cache warm through a change.
| Technique | Freshness after the write | Write-path cost | When it fails | Best fit |
|---|---|---|---|---|
| Delete-on-write | Cold, then fresh on the next read | One extra delete | A lost delete stays stale until the TTL | The default for most keys |
| Versioned keys | Fresh for new readers immediately | None; the name is the invalidation | Old keys linger until TTL, costing memory | Shapes too many to enumerate |
| Purge | A whole namespace goes cold | A burst of misses, then repopulation | The cold start lands at fleet scale | Rule changes, migrations, unknown fan-out |
| Write-through | Warm and fresh | Doubled; cache and database | A failed update stays stale until TTL | Hot keys that need read-your-writes |
The choice is per key, not global: the same per-key decision part 2 made for delivery, and the columns trade against each other in one sentence: freshness on the read path, cost on the write path, and blast radius on the failure path. The rest of this article takes the four in the order their failure modes matter.
The write path: delete-on-write, write-through, write-behind
Delete-on-write earns its workhorse status and hides the subtlest failure in the series. The ordering rule (delete after the commit) protects one race and exposes another. Consider two concurrent requests: a reader misses on user:42, reads the database, and holds the old value in memory, about to write it back; a writer commits the new value and deletes the key. The reader then completes its SET, and the stale value is back, now with a fresh TTL and the system’s full confidence. The delete was correct, the ordering was correct, and the cache is wrong anyway. The resurrection race needs no bugs, only concurrency.
Three cures exist, in ascending order of certainty. The double delete: issue the delete again after a delay longer than the read path, sweeping away whatever was resurrected in the interim: simple, effective, and forever dependent on a timer. The TTL cap: bound whatever the race resurrects, converting a permanent staleness into a window: the backstop again, doing backstop work. And versioned keys: if the write changed the key’s name along with the row, the resurrecting reader repopulates a name nobody reads anymore; the race runs harmlessly against a key that no longer matters. The race is the strongest argument for the technique the next section exists to explain.
Write-through takes the opposite bet from deletion: instead of making the copy cold and fresh, it makes the copy warm and fresh; the write path updates cache and database together, and the read-your-own-writes guarantee part 1 sketched becomes structural rather than eventual. The cost is the double write, and the failure mode is partial failure: a cache update that fails after the commit leaves a stale copy with no delete coming: the TTL backstop again, and the reason write-through deployments monitor their second write as closely as their first. Write-behind inverts the whole arrangement: the cache accepts the write, and the database learns later. Part 1 priced write-back in durability risk, part 4 called it the contract’s violation, and the honest summary is that write-behind is not an invalidation strategy at all; it is a durability strategy wearing the cache’s clothes, appropriate exactly where a durable buffer and a flush design already exist.
The read path has a role in invalidation too, because the miss is the last writer: whatever repopulates the key stamps it with a TTL, and a miss handler that writes back without an expiry converts one race into a permanent resident. The softer half of the write-path toolkit deserves its own name; serve-stale, the refresh-behind-the-response compromise: instead of deleting on write or blocking on a cold read, the system keeps serving the old copy while one request rebuilds it in the background. Part 3 priced it under stampedes; here it appears as the fifth, softest technique in the catalog: invalidation by eventual agreement rather than removal, best used with part 4’s early-expiry refinement and a TTL that bounds the disagreement.
Versioned keys: invalidation by renaming
Versioned keys solve the two problems this article opened with at once: the derivation problem and the delivery problem. The mechanism is a version embedded in the key’s name (v7:user:42) bumped when the data changes. Every old copy becomes unreachable the moment the version moves: not deleted, not broadcast, not delivered, simply no longer named. There is no message to lose, because the invalidation is not a message; the new name is the invalidation.
The mechanism has two useful granularities. Family-level: a prefix version (v2:feed:home:*) retires an entire namespace in one decision, which is a purge, the next section’s subject, performed without deleting anything: the old namespace ages out by TTL while the new one warms. Entity-level: the source row itself carries a version or an updated-at field, and the key embeds it (user:42:v7) so any write that changes the row changes the key. The derivation problem shrinks, because the version travels with the data instead of living in a mapping nobody wrote down.
The costs are three, all payable. Key churn: after every bump the new keys start cold, so a family bump is a planned cold start, and the part 3 counters (probabilistic early expiry, stampede locks) apply to it exactly as they apply to any expiry. Memory: the old, unreachable keys survive until their TTLs, and part 5’s accounting charges them the whole time, which makes long TTLs on versioned keys a memory leak with a schedule. And sprawl: versions multiply key names, so the key discipline part 4 asked for becomes the discipline this technique depends on. Versioning wins where derivations are hard to enumerate and delivery is unreliable, which, at fleet scale, is most of the places invalidation actually hurts.
Purges and bulk invalidation
A cache purge is the bluntest instrument in the catalog: remove every key in a namespace, a prefix, or a pattern, at once. Purges answer changes too broad to enumerate; a pricing rule that touches every product page, a deploy that changes how responses are computed, a data migration whose blast radius nobody wants to argue about. Where delete-on-write says “this key is wrong,” a purge says “everything of this kind might be wrong,” and the honesty of that sentence is the technique’s whole virtue.
The cost is the sentence’s other half: a purge is a self-inflicted cold start. The namespace goes cold on demand, the next reads miss together, and the database sees the fleet-wide burst that part 3 named the avalanche, the failure mode this technique deliberately invokes. The softening tactics are part 3’s counters plus one: purge in waves instead of all at once; bump the family’s version instead of deleting, letting the old namespace age out behind the new one; or seed the new namespace before switching readers to it. The mechanics matter at purge time too; part 5’s event loop makes a keyspace walk a self-inflicted stall, so the pattern is the incremental scan with background deletion, and the freed memory reclaims lazily.
And the delivery problem scales with the blast radius: a purge is a delete that must reach every node holding any key of the namespace: part 2’s broadcast question at its widest, with the same lost-message caveat and the same TTL backstop. The most robust purges are therefore not delivered at all: they are version bumps, which need no delivery because nothing needs to arrive anywhere. A purge that can be a version bump should be; a purge that must delete should be incremental, waved, and monitored; and a purge executed in a panic, unjittered, at fleet scale, is part 3’s avalanche with an incident number attached.
FAQ
What is cache invalidation?
Removing or refreshing cached copies when the data they derive from changes, so readers stop getting stale data before a TTL would have expired it on its own. Expiry is passive forgetting; invalidation is the system actively taking responsibility for its copies, which means knowing what changed, knowing who holds copies, and getting the removal to those holders at the right moment.
What are the main cache invalidation strategies?
Four deliberate techniques: delete-on-write (remove the key after the commit), versioned keys (change the key’s name so old copies become unreachable), purge (bulk removal of a namespace), and write-through (update the copy on the write path). Every one of them leans on the same backstop (a TTL on every key) for the message that never arrives.
What are versioned cache keys?
Keys whose names embed a version, a family prefix like v2:feed:* or an entity version like user:42:v7. When the version changes, the old copies are never read again and age out by TTL. It is invalidation by renaming: no deletes to deliver, no races to lose, at the price of cold new keys and lingering old ones.
When should you write through the cache instead of deleting on write?
When the key is hot enough to pay the double write and fresh enough to need read-your-writes; the copy stays warm through the change instead of going cold. The failure mode is partial failure: a cache update that fails after the commit leaves a stale copy, so the second write needs monitoring and the TTL needs to stay. Data that must be strictly fresh does not belong in a cache at all: part 1’s rule, unchanged.
How do you invalidate a cache across multiple nodes?
Three ways, from part 2: broadcast the delete to every holder (immediate, needs a delivery channel that can lose messages), gossip until the fleet converges (no fan-out machinery, a longer window), or cap divergence with TTLs (zero machinery, bounded staleness). Delivery being at-most-once in practice, the robust cross-node invalidation is the one that needs no delivery at all (a versioned key) with the TTL bounding whatever the channel drops.
Related articles
- Next read: how a CDN works; the edge sibling: HTTP caching at the network layer, where TTLs reappear as Cache-Control headers and purges as CDN invalidation.
- caching in system design, part 1: the dial this article turned, and the pattern definitions underneath the techniques.
- distributed caching, part 2: the delivery architectures every cross-node technique here relies on.
- cache stampede and failure modes, part 3: the failure taxonomy, including the avalanche a careless purge invites.
- redis caching, part 4: the Redis spelling of these techniques, and the ordering rule they assume. The names change; the discipline does not.
- redis architecture; part 5: the pub/sub transport and the incremental-scan patterns behind safe purges.
- event-driven architecture, the alternative: rebuild derived copies from a log instead of invalidating them. One pattern avoids the problem this article spends its whole budget on.