Hermes Wiki

Cache Invalidation Strategies

Concept

"There are only two hard things in Computer Science: cache invalidation and naming things" is attributed to Phil Karlton, and the reason it's stuck for decades is that it names a real structural problem: a cache is a second copy of data that a system has to actively keep honest, and every strategy for doing so trades correctness against latency, complexity, or both. Caching what to store is comparatively easy; deciding when a cached value stops being true is the hard part, because that decision has to be made without the cache itself knowing whether the underlying data changed.

Three strategies cover most of the design space:

  • TTL-based (time) expiry — every cached entry is written with an expiration timestamp; once it passes, the entry is treated as stale and refetched on the next read. This is passive: nothing has to notify the cache when data changes, the cache just stops trusting old data after a fixed window.
  • Write-through / explicit invalidation — the code path that writes new data also updates or deletes the corresponding cache entry in the same operation, so the cache is never allowed to hold a value the write path knows is wrong.
  • Event-driven invalidation — a change to the source of truth publishes an event (via a message queue, a database change-data-capture stream, a pub/sub channel), and independent consumers invalidate or refresh their own cached copies in reaction to that event, decoupling the writer from needing direct knowledge of every cache that might hold the data.

These aren't mutually exclusive — production systems commonly layer them: explicit delete-on-write for the immediate correctness guarantee, TTL expiry as a backstop against any invalidation call that gets missed (a crashed request, a bug, a direct database write that bypasses the normal write path), and pub/sub-based event invalidation for keeping caches in other processes or services in sync with a write that happened elsewhere.

Tradeoffs

Strategy Correctness guarantee Coupling Failure mode
TTL expiry only Weak — stale for up to the full TTL window after any change None — writer and cache never interact Silent staleness; nothing alerts on it, it just serves old data until the timer runs out
Write-through / explicit invalidation Strong, immediate Tight — every write path must know about and touch every affected cache key A missed invalidation call (new code path, direct DB write, partial failure) leaves permanently stale data with no self-correction
Event-driven invalidation Strong, near-immediate, and decoupled Loose — writer only publishes an event, doesn't need to know who's listening Depends on the event pipeline's own reliability; a dropped or delayed event reproduces the same staleness TTL alone would have, just less predictably

The pattern across all three is the same triangle as any caching decision: the stronger the correctness guarantee, the more machinery (explicit call sites, an event pipeline) has to be built and kept working. TTL alone is the cheapest to implement and the easiest to reason about, but it's a guarantee about the maximum staleness window, not about correctness at any given moment inside that window.

When to use / when not to

  • Default to TTL-based expiry for data where a bounded staleness window is genuinely acceptable to the product — a listing page, a computed aggregate, a "last updated N minutes ago" style dataset. It requires zero coordination with writers and degrades gracefully.
  • Add explicit write-through invalidation on the specific paths where a user (or another system) reasonably expects to see their own write reflected immediately — a user saving their own settings and reloading the page, an admin toggling a feature flag that should take effect now, not in five minutes.
  • Reach for event-driven invalidation once more than one process or service needs to react to the same underlying change — a single write-through call only updates the cache the writer itself knows about; a fan-out of independent caches (multiple service instances, multiple downstream caches) needs a pub/sub or CDC-based signal instead.
  • Don't rely on write-through alone with no TTL backstop — any code path that writes data without going through the "official" write function (a backfill script, a direct database migration, an emergency hotfix) silently produces permanently stale cache entries with no mechanism to self-correct.
  • Don't build event-driven invalidation for a single-process, single-cache system where write-through invalidation already gives the same guarantee more simply — the added infrastructure (a message broker, delivery guarantees, ordering concerns) isn't buying anything additional in that case.

Common pitfall

Treating explicit invalidation as sufficient on its own and skipping a TTL backstop, on the reasoning that "every write goes through the cache-invalidation code, so it's always covered." That reasoning holds right up until it doesn't — a new write path gets added without the cache team knowing, a background job writes directly to the database for a one-off fix, or a partial failure (the DB write succeeds, the process crashes before the cache-invalidation call runs) leaves a specific key stale indefinitely. Because nothing in an explicit-only system re-checks freshness on its own, this class of staleness has no natural expiry — it can persist until someone notices the wrong data and manually investigates. A TTL on every cached entry, even a generous one, turns "permanently stale until discovered" into "stale for at most N minutes," which is a materially different production risk profile for a small amount of extra design effort.

Engineering Lens

The strongest tell in a design review isn't which single strategy someone picked — it's whether they can name the actual staleness bound their system tolerates for a given piece of data, and show that the invalidation strategy chosen actually delivers that bound under real failure modes, not just the happy path. "We invalidate on write" sounds complete until asked "what happens if that write path is bypassed" — the answer that names TTL-as-backstop, or an explicit reconciliation job, is the one that reflects having actually operated a cache through an incident, not just implemented one against the spec.

Sources

Hermes Wiki