Cache Invalidation and Consistency
Adding a cache is easy; keeping it correct is one of the famously hard problems in computing. Every cache holds a copy of data that lives somewhere else, and the moment the source changes, the copy may be wrong. A product shows yesterday's price, a user changes their name and still sees the old one, a permission is revoked but a cached session keeps working. Most caching bugs are not about speed at all — they are about stale or inconsistent data.
There is no single correct invalidation strategy. The right approach depends on how stale each kind of data may be, how it changes, and how many cache layers sit between the source and the user. This page covers the strategies, the race conditions that undermine them, and how to combine them.
TL;DR
- Decide how much staleness each kind of data can tolerate before choosing a strategy.
- TTL bounds staleness simply; delete-on-write shortens it; combine both as a safety net.
- Delete cache entries on change rather than writing new values, to avoid races between writers.
- Use events (pub/sub, CDC) to invalidate caches in other services and in-process caches on every node.
- Versioned keys and tag-based purges invalidate groups of entries without enumerating them.
- Guard against the read-repopulate race with short TTLs, leases, or version checks, and handle read-your-writes explicitly.
Quick Example
Updating a product, then invalidating every cache layer after the database commit:
The HTTP response that put the page in the CDN carried the tag that makes the purge possible:
Even if one invalidation message is lost, the five-minute TTL bounds how long anyone sees the old price.
Core Concepts
Staleness Budgets
Writing these budgets down turns invalidation from a guessing game into a design decision.
Invalidation Strategies
Stale-While-Revalidate
Serving a slightly stale value while refreshing it in the background keeps latency low and avoids stampedes. HTTP's stale-while-revalidate directive does this at the browser and CDN level; application caches can implement it with soft and hard TTLs.
Race Conditions
The Read-Repopulate Race
The classic cache-aside bug:
Mitigations:
- Short TTLs bound the damage.
- Leases: the cache gives the first missing reader a token, and invalidation voids it, so the stale set is rejected (the approach popularized by Facebook's Memcache work).
- Versioned values: store the source version with the cached value and refuse to overwrite a newer version with an older one.
- Delayed second delete: delete again shortly after the write to catch stragglers — cheap, though not a guarantee.
Concurrent Writers
When writers update the cache directly (write-through), two concurrent writes can land in the cache in the opposite order from the database. Deleting instead of setting, or using compare-and-set with versions, avoids leaving the older value behind.
Invalidating Before Commit
Deleting the cache key before the database transaction commits lets another request re-cache the old value from the not-yet-updated database. Invalidate after commit — ideally from the committed change itself, through an outbox or change data capture.
Multi-Layer Caches
Invalidate from the inside out: shared caches first, then CDN, while browser caches are controlled only through short lifetimes and revalidation. See Caching Layers and CDN.
Read-Your-Writes
Users expect to see their own changes immediately, even when other users can tolerate seconds of staleness. Options include bypassing the cache for the writer's next reads, reading from the primary database for a short window after a write, returning the updated object from the write and rendering it directly, or including a version in the session that forces fresher reads.
Best Practices
Always Keep a TTL as a Safety Net
Event-driven invalidation can miss messages. A TTL guarantees that stale data eventually disappears.
Invalidate From the Source of Change
Drive invalidation from committed changes — outbox events or CDC — rather than from scattered application code paths that are easy to forget.
Make Keys Derivable
Design keys so every writer can compute which keys an update affects. If invalidation requires scanning the cache, the key design needs work.
Monitor Staleness, Not Just Hit Rate
Track invalidation lag and sample cached values against the source. A high hit rate on wrong data is a failure.
Common Mistakes
Caching Authorization Decisions Too Long
Updating the Cache Instead of Deleting
Forgetting Derived Keys
FAQ
Why is cache invalidation hard?
Because caches hold copies of data that changes elsewhere, often across many layers and nodes, with concurrent readers and writers. Ensuring every copy is updated or removed at the right time, without races, is difficult in distributed systems.
Should I update or delete the cache on writes?
Deleting is usually safer: it avoids races between concurrent writers and leaves repopulation to the next read. Write-through can work when writes are serialized or versioned.
What TTL should I use?
The longest staleness the data can tolerate, informed by how often it changes and how bad stale data is. Use TTLs as a safety net even when you also invalidate explicitly.
How do I invalidate in-memory caches on many servers?
Broadcast invalidation messages through pub/sub (for example Redis pub/sub or a message broker) and keep in-process TTLs short, so a missed message only causes brief staleness.
How do I invalidate CDN caches?
Purge by URL, or better, by tag: attach tags such as product-42 to responses and purge all entries with that tag when the product changes. Versioned URLs for static assets avoid purging entirely.
Related Topics
- Caching — Caching strategies across the stack
- Redis Caching Patterns — Cache-aside, write-through, and stampedes
- Caching Layers — Where caches live
- CDN — Edge caching and purges
- Change Data Capture — Invalidation driven by database changes
- Memcached — A distributed cache with leases in its history