On this page
  1. What is cache invalidation?
  2. The four strategies, and what each costs
  3. Why an invalidation bug is usually a missing path
  4. How to choose
  5. Where AI coding assistants get this wrong
  6. Checklist
  7. FAQ
    1. Is it acceptable to cache things without invalidating them?
    2. Why does my cached data go stale even though I purge on write?
    3. Should I delete the cache entry or update it on write?
    4. How short should a time-to-live be?
  8. Related topics
  9. Sources
concept

Cache Invalidation Explained

Stale data is the price of caching. Here are the strategies that keep it bounded — TTL, write-through, purge on write, versioned keys — and how to choose between them.

Quick answer

  • Cache invalidation is deciding when a cached value stops being used, either by expiring it after a time or by removing it when the underlying data changes.
  • The four practical strategies are time-to-live expiry, purge on write, write-through (update the cache as you write), and versioned keys that make old entries unreachable.
  • Time-to-live is simple and bounds staleness by a known maximum; it is the right default when a slightly old value is acceptable.
  • Purge or version on write is for data that must be correct immediately, and it is the one that fails most often when a code path forgets to do it.
  • The safest design combines a short time-to-live as a backstop with explicit invalidation on write, so a missed purge expires rather than persisting.

What is cache invalidation?

It is the policy that decides when a cached copy is no longer used. Every cache has one whether or not it was designed: if nothing else, the entry stays until something evicts it for space. Making it explicit turns an unpredictable staleness window into a number you chose, which is the difference between a cache that helps and one that produces bug reports about data that is out of date.

The decision has two dimensions: how stale a value may be, and how expensive it is to keep the copy current. A profile picture can be minutes out of date; an account balance cannot. A value read constantly is worth invalidating carefully; one read once an hour barely needs a cache at all. Choosing a strategy follows from those two answers rather than from a preference for a technique.

The four strategies, and what each costs

Expiry by time-to-live is the simplest: the entry is used until its lifetime ends, then discarded and recomputed. Staleness is bounded by the lifetime, no invalidation code is needed, and the failure mode is benign — you serve a value that is at most as old as the lifetime. Its weakness is that a long lifetime means real staleness, and a short one means frequent recomputation.

Purge on write removes the entry when the underlying row changes, so the next read rebuilds it. It gives near-immediate consistency and costs a coordination step on every write, and it fails silently when a write path forgets. Write-through instead updates the cache as part of the write, which keeps the entry warm but requires the write path to know the cache’s key and shape. Versioned keys sidestep deletion entirely: include a version or timestamp in the key so a new write produces a new key and the old entry simply becomes unreachable, then expires.

Strategy          | Staleness            | Cost on write        | Failure mode
------------------|----------------------|----------------------|---------------------------
TTL expiry        | Bounded by TTL       | None                 | Visible up to TTL
Purge on write    | Near zero            | Delete per write     | Missed purge = stale until
                  |                      |                      | TTL or eviction
Write-through     | Near zero            | Update per write     | Write path must know key
Versioned key     | Near zero            | New key per write    | Old keys accumulate space
Stale-while-      | Bounded by TTL +     | Background refresh   | Extra read latency on the
revalidate        | refresh              |                      | first request after expiry

Why an invalidation bug is usually a missing path

The common bug is not choosing the wrong strategy; it is that one of several write paths does not perform the invalidation the others do. A user updates their profile through the settings page and the cache is purged; an admin updates it through a support tool and it is not. The cache is then correct for one route and stale for another, which looks like intermittent data corruption and is very hard to reproduce.

The defence is to put invalidation where it cannot be forgotten: in the data layer or the model, not in the handler, so every write goes through it regardless of the route. Where that is impractical, the second defence is a time-to-live short enough that the worst case is a delay rather than a permanent wrong answer. A cache with no expiry and multiple write paths is the configuration that produces the incidents.

How to choose

If a slightly old value is acceptable, use a time-to-live and set it to the longest staleness you can tolerate. If it is not, invalidate on write, and add a time-to-live anyway as a backstop for the path you did not think of. If the value is a computed aggregate that is expensive and only needs to be roughly current, use a longer lifetime plus an explicit refresh job rather than invalidation. If the data is immutable once written, cache it forever and never worry about it.

Two habits make all of these safer. Include the cache key in the thing you invalidate, so purges are precise rather than a flush of the whole namespace — a full flush is a load spike dressed as a fix. And emit a metric for hit rate and for staleness-sensitive errors, so you find out from a graph when invalidation stops working rather than from a user telling you their name is wrong.

Where this bites vibecoders

Generated code caches where it is easy: a decorator on a read function, with a default lifetime, and nothing that knows about writes. That is the strategy with the widest staleness window and no invalidation at all, so the bug arrives later as “the app shows old data” and is hard to attribute. Asking where each cached value is written, and whether that write clears it, is a short review that catches the whole class.

Where AI coding assistants get this wrong

  • Adding a cache with a long or infinite lifetime and no invalidation.
  • Invalidating the cache in one write path and forgetting the others.
  • Flushing the entire cache namespace instead of the affected keys, causing a load spike.
  • Caching per-user data under a key that does not include the user.
  • Invalidating before a transaction commits, so a concurrent read caches the old value.
  • Having no metric for hit rate or staleness, so failures are invisible.

Checklist

  • List every cached value and the write paths that change it.
  • Put invalidation in the data layer so no path can skip it.
  • Set a time-to-live as a backstop even where you invalidate.
  • Invalidate after the transaction commits, not before.
  • Scope purges to the affected keys.
  • Track hit rate and alert on a sudden drop.

FAQ

Is it acceptable to cache things without invalidating them?

Yes, when a bounded staleness window is acceptable and the time-to-live is short enough to be the whole answer. That is a deliberate choice rather than an omission. It stops being acceptable when the value must be correct immediately, which is exactly when a missing invalidation becomes a bug report.

Why does my cached data go stale even though I purge on write?

Usually because one write path does not purge, or because a read repopulates the cache from a replica before replication catches up. Check every path that writes the value, and invalidate after the primary commit rather than inside the transaction. A short time-to-live remains the reliable backstop.

Should I delete the cache entry or update it on write?

Deleting is safer and simpler: the next read rebuilds the correct value, and there is no chance of writing a shape the read path does not expect. Updating in place saves one read but requires the write path to know the exact key and format. Prefer deletion unless the read is expensive and the write is frequent.

How short should a time-to-live be?

As long as the business can tolerate being wrong. That is the only input that matters. Start from the staleness the user would notice, halve it for safety, and measure the hit rate at that setting — if the cache stops helping, the number is too short and the real problem is that the value is not cache-friendly.

Sources

Share: