Caching Is Easy Until You Have to Invalidate It

Key takeaway: The difficulty of caching is not in storing data but in knowing when it is no longer valid, and a cache that serves stale data is often worse than no cache at all, because it presents outdated information as though it were current.
Why Invalidation Is the Hard Part
Caching is conceptually simple — store the result of an expensive computation or query, and serve it from the cache on subsequent requests instead of recomputing it. The difficulty is that the underlying data changes, and the cache needs to know when a cached value no longer reflects the current state, so it can be refreshed or removed.
Getting this wrong produces stale data — a user sees an old value that no longer matches what is actually stored, with no indication that it is outdated. This is frequently worse than no cache, because the user has no way to know the data is stale and may act on it as though it were current, and the bug is hard to diagnose because the data looks plausible.
The Main Invalidation Strategies
| Strategy | How it works | Trade-off |
|---|---|---|
| Time-based (TTL) | Cached value expires after a fixed time | Simple, but serves stale data until expiry |
| Write-through | Cache updated on every write | Always fresh, but adds write latency |
| Explicit invalidation | Cache entry removed when data changes | Precise, but requires knowing all dependencies |
| Version-based | Cache key includes a version that changes on update | Precise, but requires propagating versions |
Time-based expiry is the simplest and most common, and its trade-off is that it serves stale data for up to the full TTL after a change — acceptable when the data changes rarely or when slight staleness is tolerable, and a problem when the data changes frequently and users expect to see recent changes immediately.
The Hardest Case: Explicit Invalidation
Explicit invalidation — removing a cache entry at the moment the underlying data changes — is the most precise approach, and it is also the hardest to get right, because it requires knowing every cache entry that could be affected by a given change. A single write can invalidate many cached values — a user profile change affects the profile page, any list that includes the user, any search result — and missing even one of these dependencies leaves a stale entry that will be served until it expires.
This is why cache invalidation is often described as one of the two hard problems in computer science — the dependency graph between writes and cached values is easy to underestimate, and the bugs that result are subtle and intermittent, appearing only when a specific write happens to be followed by a read of a cached value that should have been invalidated but was not.
A Practical Approach
A pragmatic combination is to use time-based expiry as a safety net, with explicit invalidation for the specific, well-understood cases where staleness is most visible or most harmful — this gives the simplicity of TTL as a backstop while addressing the cases where it matters most precisely, without requiring a perfect dependency graph for every single cached value.
The Bottom Line
Treat cache invalidation as the primary design problem when adding a cache, since serving stale data is often worse than not caching at all. Use time-based expiry as a simple default with a safety net, add explicit invalidation for the cases where staleness is most visible or harmful, and be deliberate about the dependency graph between writes and cached values, since missing a dependency is the most common source of subtle cache bugs.



