Cache Invalidation
Keeping a copy honest when the original changes.
Introduction
Cache invalidation is the discipline of making sure cached copies don't outlive their truth. Every cache holds a copy; the moment the source of truth changes, that copy is a potential lie. Deciding when and how to refresh or remove it is famously one of the hardest problems in computing.
Why it exists
A cache exists because reads outnumber writes — but writes still happen. Without invalidation, users see prices, permissions, or inventory that changed minutes or hours ago. Invalidation exists to bound and control that staleness so the performance win of caching doesn't turn into a correctness bug.
Analogy
It's like a printed price tag on a shelf. When head office changes the price (the source of truth), the shelf tag is now wrong. Someone must reprint it (update), pull it (invalidate), or you accept it's only re-checked each morning (TTL). Every store chooses one of these policies — and so does every cache.
How it works
There are three broad strategies. TTL (expiration): give each entry a lifetime and let it expire; simple and self-healing, but data can be stale up to the TTL. Write invalidation: when the source changes, delete or update the affected cache entries; fresher, but you must know every key affected by a write. Event-driven invalidation: the source publishes change events (or uses change-data-capture) and caches subscribe and evict — good across many caches and regions, at the cost of infrastructure.
The deep problem is ordering under concurrency. The common 'update DB, then delete cache' can still race: a reader that missed just before the write can repopulate the cache with the old value just after the delete. Techniques like delete-then-write with short TTLs, versioned keys, or leases bound this window. In distributed setups you also choose between per-node caches (evict everywhere via events) and a shared cache (evict once).
- 1
A write updates the source of truth (database) and commits.
- 2
The writer invalidates the affected cache key(s) — typically a delete.
- 3
A subsequent read misses and repopulates the cache from the fresh source.
- 4
Other caches/regions receive an event and evict their copies (if event-driven).
- 5
A TTL acts as a safety net so any missed invalidation self-heals eventually.
Interactive
Architecture
Write path with invalidation
1. Persist truth
2. Invalidate copy
3. Next read repopulates
Deleting on write (rather than overwriting) lets the next read pull the fresh value and sidesteps some write-order races.
In code
import { redis } from "@/lib/redis"
import { db } from "@/lib/db"
export async function updateProduct(id: string, patch: ProductPatch) {
// 1. Source of truth first.
const updated = await db.product.update(id, patch)
// 2. Invalidate rather than overwrite: the next read repopulates fresh.
// Overwriting here could lose a race with a concurrent reader's stale set.
await redis.del(`product:${id}`)
// 3. Invalidate derived/aggregate keys this write affects.
await redis.del(`category:${updated.categoryId}:products`)
return updated
}// Bump a version on write; reads always target the current version.
async function currentKey(id: string) {
const v = await redis.get(`product:${id}:ver`) ?? "1"
return `product:${id}:v${v}`
}
export async function bumpVersion(id: string) {
await redis.incr(`product:${id}:ver`) // old versioned keys are now unreachable; TTL reaps them
}In the real world
Where you've seen this
A CMS caches rendered pages at the CDN. When an editor publishes an edit, the app calls the CDN's purge API for that URL (write invalidation) and also relies on a max-age so any un-purged copy self-heals. E-commerce inventory uses event-driven invalidation: an inventory-changed event evicts product and listing caches across all regions.
When to use it
Reach for it when
- TTL: when bounded staleness is acceptable and writes are hard to track precisely.
- Write invalidation: when you can identify exactly which keys a write affects and want freshness.
- Event-driven: when many caches/regions must stay consistent with a changing source.
Avoid it when
- Don't build complex event-driven invalidation when a short TTL is good enough.
- Don't try to invalidate data you can't reliably enumerate the keys for — use TTL instead.
- Don't invalidate at all for immutable data — cache it forever (or version the URL).
Trade-offs
Advantages
- Keeps cached data honest, preserving the performance win without correctness bugs.
- TTL is self-healing and trivial to reason about.
- Event-driven invalidation scales consistency across regions.
Disadvantages
- Correct write-path invalidation is genuinely hard (which keys? what order?).
- Concurrency races can repopulate stale values.
- Event pipelines add infrastructure and their own failure modes.
- Over-invalidation destroys hit ratio; under-invalidation serves stale data.
The master trade-off is freshness vs simplicity vs hit ratio. TTL is simple and self-healing but staleness-prone; write invalidation is fresh but requires perfectly knowing affected keys and handling races; event-driven is consistent across regions but is real infrastructure. Longer TTL and coarse invalidation raise hit ratio but stale risk; aggressive invalidation lowers stale risk but hammers the origin. Most production systems combine a short TTL safety net with targeted write invalidation.
| Invalidation strategies | TTL | Write invalidation | Event-driven |
|---|---|---|---|
| Freshness | Bounded by TTL | High | High across regions |
| Complexity | Very low | Medium (which keys?) | High (pipeline) |
| Self-healing | Yes | No (needs TTL backup) | Depends on delivery |
| Best for | Tolerable staleness | Known key sets | Many caches/regions |
Common mistakes
Watch out for
- Overwriting the cache on write instead of deleting, creating a stale-set race.
- Forgetting derived/aggregate keys (lists, counts) that the write also affects.
- No TTL safety net, so a single missed invalidation stays wrong forever.
- Invalidating before the DB commit, so a reader caches the pre-commit value.
- Per-user data cached under shared keys and never correctly invalidated.
Failure thinking
What breaks it
You update the DB then delete the cache, but a read still returns stale data. How?
A reader missed the cache just before your write committed, read the old value, and then set it into the cache just after your delete — repopulating stale. Bound this with a short TTL (so it self-heals fast), delete again after a small delay (double-delete), use versioned keys, or use a lease so only a read that holds a valid lease may populate. Naming this race is the core of the topic.
A cache holds stale permissions after an admin revokes access. What's the risk and fix?
The user keeps access until the entry expires — a security bug. For security-sensitive data prefer very short TTLs or synchronous write invalidation on the exact keys, and consider not caching authorization decisions at all, or caching them with an explicit event-driven purge on role changes.
Think like a senior
Senior Engineer Insight
The senior move is layering: targeted write invalidation for freshness plus a short TTL as a safety net for the invalidations you inevitably miss. Relying on either alone is fragile.
Senior Engineer Insight
Prefer designs that avoid invalidation: immutable, content-addressed, or versioned keys never go stale because a change produces a new key. When you can, make the problem disappear rather than solving it.
Remember
Update the source of truth first, then invalidate — never before the commit.
Remember
Delete, don't overwrite, and always keep a TTL as a safety net.
Interview questions
How do you invalidate a cache correctly on writes?
Active recall
Check yourself
A popular key expires and thousands of concurrent requests all miss the cache and hit the database at once. What is this called, and a common fix?
Practical challenges
Reproduce and fix the stale-set race
Demonstrate the cache-aside write race, then bound it.
Requirements
- Construct a scenario: reader misses, write commits + deletes, reader sets stale value.
- Show the cache now holds stale data.
- Apply a fix and show it no longer happens (or self-heals quickly).
Acceptance criteria
- After the fix, stale data is impossible or bounded to a small, defined window.
Edge cases
- The fix's leader/lease holder crashes mid-populate.
Bonus
- Implement versioned keys and show the race disappears entirely.
Reflection
- Why does deleting instead of overwriting help but not fully solve the race?
Architecture challenge
A product's price is cached in Redis (per-item), in a category listing (aggregate), at the CDN (rendered page), and in each app instance's local memory. A price change must reach users quickly and consistently. Design the invalidation across all four layers, decide the ordering, and specify what self-heals if one purge fails.
Flashcards
Summary
Cache invalidation keeps cached copies consistent with a changing source using TTLs, write-time invalidation, or change events — usually in combination. The genuine difficulty is knowing exactly which keys a write affects and handling concurrent read/write ordering, which is why layering targeted invalidation with a short TTL safety net (or designing with versioned keys) is the pragmatic answer.
Key takeaways
- Commit the source of truth first, then invalidate.
- Delete affected keys (including derived ones) rather than overwriting.
- Always keep a TTL as a self-healing safety net.
- Versioned/immutable keys make invalidation races disappear.
Your notes
Saved to this device