Cache Invalidation and Consistency

Adding a cache is easy; keeping it correct is one of the famously hard problems in computing. Every cache holds a copy of data that lives somewhere else, and the moment the source changes, the copy may be wrong. A product shows yesterday's price, a user changes their name and still sees the old one, a permission is revoked but a cached session keeps working. Most caching bugs are not about speed at all — they are about stale or inconsistent data.

There is no single correct invalidation strategy. The right approach depends on how stale each kind of data may be, how it changes, and how many cache layers sit between the source and the user. This page covers the strategies, the race conditions that undermine them, and how to combine them.

TL;DR

Quick Example

Updating a product, then invalidating every cache layer after the database commit:

The HTTP response that put the page in the CDN carried the tag that makes the purge possible:

Even if one invalidation message is lost, the five-minute TTL bounds how long anyone sees the old price.

Core Concepts

Staleness Budgets

Writing these budgets down turns invalidation from a guessing game into a design decision.

Invalidation Strategies

Stale-While-Revalidate

Serving a slightly stale value while refreshing it in the background keeps latency low and avoids stampedes. HTTP's stale-while-revalidate directive does this at the browser and CDN level; application caches can implement it with soft and hard TTLs.

Race Conditions

The Read-Repopulate Race

The classic cache-aside bug:

Mitigations:

Concurrent Writers

When writers update the cache directly (write-through), two concurrent writes can land in the cache in the opposite order from the database. Deleting instead of setting, or using compare-and-set with versions, avoids leaving the older value behind.

Invalidating Before Commit

Deleting the cache key before the database transaction commits lets another request re-cache the old value from the not-yet-updated database. Invalidate after commit — ideally from the committed change itself, through an outbox or change data capture.

Multi-Layer Caches

Invalidate from the inside out: shared caches first, then CDN, while browser caches are controlled only through short lifetimes and revalidation. See Caching Layers and CDN.

Read-Your-Writes

Users expect to see their own changes immediately, even when other users can tolerate seconds of staleness. Options include bypassing the cache for the writer's next reads, reading from the primary database for a short window after a write, returning the updated object from the write and rendering it directly, or including a version in the session that forces fresher reads.

Best Practices

Always Keep a TTL as a Safety Net

Event-driven invalidation can miss messages. A TTL guarantees that stale data eventually disappears.

Invalidate From the Source of Change

Drive invalidation from committed changes — outbox events or CDC — rather than from scattered application code paths that are easy to forget.

Make Keys Derivable

Design keys so every writer can compute which keys an update affects. If invalidation requires scanning the cache, the key design needs work.

Monitor Staleness, Not Just Hit Rate

Track invalidation lag and sample cached values against the source. A high hit rate on wrong data is a failure.

Common Mistakes

Caching Authorization Decisions Too Long

Updating the Cache Instead of Deleting

Forgetting Derived Keys

FAQ

Why is cache invalidation hard?

Because caches hold copies of data that changes elsewhere, often across many layers and nodes, with concurrent readers and writers. Ensuring every copy is updated or removed at the right time, without races, is difficult in distributed systems.

Should I update or delete the cache on writes?

Deleting is usually safer: it avoids races between concurrent writers and leaves repopulation to the next read. Write-through can work when writes are serialized or versioned.

What TTL should I use?

The longest staleness the data can tolerate, informed by how often it changes and how bad stale data is. Use TTLs as a safety net even when you also invalidate explicitly.

How do I invalidate in-memory caches on many servers?

Broadcast invalidation messages through pub/sub (for example Redis pub/sub or a message broker) and keep in-process TTLs short, so a missed message only causes brief staleness.

How do I invalidate CDN caches?

Purge by URL, or better, by tag: attach tags such as product-42 to responses and purge all entries with that tag when the product changes. Versioned URLs for static assets avoid purging entirely.

Related Topics

References