Cache Invalidation
In a Nutshell
"There are only two hard things in Computer Science: cache invalidation and naming things." The joke endures because cache invalidation — deciding when a cached copy is no longer valid and getting rid of it — is genuinely hard. Cache too aggressively and you serve stale data; invalidate too eagerly and you lose the benefit of caching. The core tension is between freshness (users see current data) and efficiency (fewer origin hits). Invalidation strategies range from simple time-based expiry (TTL) to explicit event-driven deletion, each trading complexity for control over exactly how stale your data can get.

How It Actually Works
The Three Ways Cached Data Goes Wrong
| Problem | Cause |
|---|---|
| Stale data | Underlying source changed but cache still holds the old value |
| Cache stampede | Many requests miss at once and overwhelm the origin |
| Inconsistency across nodes | Local caches on different servers hold different versions |
Invalidation strategy is mostly about controlling the first (staleness) without triggering the second (stampede).
Invalidation Strategies
| Strategy | How | Freshness | Complexity |
|---|---|---|---|
| TTL (time-based) | Entry expires after N seconds | Bounded staleness (up to TTL) | Lowest |
| Explicit / event-driven | Delete the key when the source changes | Near-immediate | Higher (must hook all writes) |
| Write-through | Update cache on every write | Always fresh | Write overhead |
| Versioned keys | Change the key when data changes | Always fresh (old key ignored) | Key management |
| Purge / bulk | Clear a group of keys (e.g., by tag) | Immediate | Tracking dependencies |
TTL: The Workhorse
The simplest and most robust strategy: give each entry an expiry. It self-heals (stale data can't live longer than the TTL) and needs no coordination with writes. The only decision is how long:
Short TTL (seconds): fresher data, more origin load
Long TTL (hours): less origin load, more staleness
Choose per data type by "how stale can this be?":
Stock ticker → 1s (must be fresh)
Product details → 5–15m (rarely changes)
Country list → 24h (essentially static)
TTL alone is often enough — many systems never need explicit invalidation because bounded staleness is acceptable.
Explicit Invalidation: When You Need Freshness Now
When staleness up to the TTL is unacceptable, delete the key the moment the source changes. The safe pattern (from Cache Read/Write Patterns) is write to DB, then delete the cache key — the next read repopulates from fresh data. Combine with a short TTL as a safety net in case a delete is missed.
The Distributed Invalidation Problem
Explicit invalidation gets hard when caches are spread across many servers (especially local caches):
Server A and Server B each have a local copy of "user:42".
User updates their profile → Server A deletes its copy.
❌ Server B still serves the STALE copy.
Solutions:
• Use a shared distributed cache (Redis) so there's ONE copy to invalidate.
• Publish invalidation events (pub/sub) so every node evicts its local copy.
• Keep local-cache TTLs very short so divergence self-heals quickly.
This is a strong argument for a shared cache tier over per-node local caches when freshness matters.
Stale-While-Revalidate: Freshness Without the Stampede
A powerful middle ground: serve the slightly-stale cached value immediately while asynchronously refreshing it in the background. Users never wait on a miss, and the cache stays reasonably fresh.
Request for expired-but-present key:
→ return stale value NOW (fast, no user-visible miss)
→ trigger a background refresh from the origin
→ future requests get the fresh value
Avoids both the latency spike AND the stampede of a hard expiry.

Seeing It in Action
Scenario: Invalidation strategy for a news site with mixed freshness needs.
Article body (rarely edited after publish):
→ TTL 10 min + explicit invalidation on edit.
Editors' changes appear near-instantly (explicit delete);
the TTL is a safety net if an invalidation event is lost.
Article view count (updates constantly, staleness OK):
→ TTL 30s, no explicit invalidation.
A count that's 30s old is perfectly acceptable; not worth the
complexity of invalidating on every increment.
Homepage / section fronts (curated, high traffic):
→ stale-while-revalidate.
Serve the cached front instantly; refresh in the background.
A viral spike can't stampede the origin because only one
background refresh runs at a time.
Breaking-news banner (must be current):
→ very short TTL (5s) + explicit purge on publish.
Purge propagated via Redis pub/sub to all edge/app caches.
Multi-server local caches:
→ Invalidation events published to a Redis channel; every app
server subscribes and evicts its local copy on an edit.
The design principle: there is no single right invalidation strategy — you choose per data type based on how much staleness is acceptable and how expensive the source is. TTL handles the easy majority; explicit invalidation and pub/sub handle the freshness-critical minority; stale-while-revalidate handles the high-traffic case where you want both freshness and stampede protection.
Interview Questions
Q: Why is cache invalidation considered hard? Hint: It's the tension between freshness and efficiency with no perfect answer: cache too long → stale data; invalidate too eagerly → lose caching benefit and risk stampedes. It's worse in distributed settings where many nodes hold copies that must all be evicted consistently, and you must hook every write path that could change the source data. Missing one path silently serves stale data.
Q: When is TTL-based expiry sufficient, and when do you need explicit invalidation? Hint: TTL suffices when bounded staleness is acceptable — the data can be up to N seconds old without harm (view counts, catalogs, config). It's simple and self-healing. Use explicit invalidation (delete on write) when staleness up to the TTL is unacceptable and changes must appear near-immediately (edited content, price changes). Often combine both: explicit delete + short TTL safety net.
Q: What's the safe ordering for invalidating a cache on a write, and why? Hint: Write to the database first, then delete (invalidate) the cache key — don't update it. Deleting avoids the races that updating causes (concurrent writers leaving a stale value), because a missed/spurious delete only triggers a harmless re-fetch. The next read repopulates from the fresh DB value.
Q: How do you invalidate caches that live locally on many servers? Hint: Local per-node caches can't be deleted from one place. Options: use a shared distributed cache (Redis) so there's a single copy to invalidate; publish invalidation events via pub/sub so every node evicts its local copy; or keep local TTLs very short so divergence self-heals. Freshness needs favor a shared cache tier over per-node local caches.
Q: What is stale-while-revalidate and what problem does it solve? Hint: On an expired-but-present entry, serve the stale value immediately while triggering an asynchronous background refresh. It eliminates the user-visible latency of a cache miss and prevents cache stampedes (only one refresh runs), giving near-fresh data without a hard-expiry cliff. Ideal for high-traffic, expensive-to-compute resources like homepages.
References
- Cloudflare: Cache invalidation strategies — TTL, purge, and tags
- MDN: HTTP Caching (Cache-Control, stale-while-revalidate) — the header-level mechanisms
- Facebook: Scaling Memcache — invalidation at scale
Dive Deeper
- Martin Kleppmann: Caches, invalidation, and consistency — the deeper consistency issues
- RFC 5861: stale-while-revalidate & stale-if-error — the HTTP directives
- Cache stampede (probabilistic early expiration) — algorithmic mitigation