Expert14 min readLevel 2

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. 1

    A write updates the source of truth (database) and commits.

  2. 2

    The writer invalidates the affected cache key(s) — typically a delete.

  3. 3

    A subsequent read misses and repopulates the cache from the fresh source.

  4. 4

    Other caches/regions receive an event and evict their copies (if event-driven).

  5. 5

    A TTL acts as a safety net so any missed invalidation self-heals eventually.

Interactive

Architecture

Write path with invalidation

Writerupdate request

1. Persist truth

Databasecommit change

2. Invalidate copy

CacheDEL affected keys

3. Next read repopulates

Next readermiss → fresh value

Deleting on write (rather than overwriting) lets the next read pull the fresh value and sidesteps some write-order races.

In code

TypeScriptDelete-on-write to avoid a stale race
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
}
TypeScriptVersioned keys sidestep invalidation entirely
// 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 strategiesTTLWrite invalidationEvent-driven
FreshnessBounded by TTLHighHigh across regions
ComplexityVery lowMedium (which keys?)High (pipeline)
Self-healingYesNo (needs TTL backup)Depends on delivery
Best forTolerable stalenessKnown key setsMany 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

1

How do you invalidate a cache correctly on writes?

Active recall

Check yourself

Cache Invalidation · Question 1 / 1Hard

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

Failure drill1–3 hours

Reproduce and fix the stale-set race

Demonstrate the cache-aside write race, then bound it.

Under concurrency, 'update DB then delete cache' can still leave a stale value in the cache.

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

Flashcards1 / 4

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