The two pieces before this one described how caches quietly break: an invalidation path nobody accounted for, a key that left out a dimension that mattered. Both failures share a root cause worth naming directly — the caching strategy was more sophisticated than the team’s ability to reason correctly about every case it covered. This suggests a rule that runs against the instinct to build something impressive: for most systems, the right amount of caching cleverness is very little, and the boring choice — short time-based expiry, a key with everything obviously relevant in it, no elaborate invalidation graph — is usually the correct engineering decision, not a fallback for when you haven’t gotten around to the sophisticated version.

The appeal of clever invalidation is real and easy to understand: it promises always-fresh data with maximum cache benefit, no wasted work, no serving anything even slightly stale. But that promise is paid for with exactly the complexity the last two pieces described — more paths to track, more places invalidation logic needs to be triggered, more surface area for the one missed case that lets the cache quietly diverge from the truth. A short TTL, in contrast, makes a much weaker promise — this might be up to sixty seconds stale — and keeps that promise trivially, because expiry doesn’t depend on catching every mutation path, it just depends on a clock, which is about as hard to get subtly wrong as anything in a system can be.

This trade is worth making explicit rather than assumed away: TTL-based caching accepts bounded staleness in exchange for correctness that’s almost mechanical to verify, while invalidation-based caching promises zero staleness in exchange for a correctness argument that has to hold across every single mutation path, including the ones added eighteen months from now by someone who’s never heard of the cache. For a large fraction of real use cases, bounded staleness costs nothing anyone notices — a sixty-second-old count of total signups, a five-minute-old list of trending items — while the complexity of chasing perfect freshness costs real engineering time and real risk of the exact silent-wrongness bugs the earlier pieces described. The clever version isn’t free even when it works; it’s a standing maintenance obligation that has to keep being right forever, while the boring version just has to keep expiring correctly, which it does by construction.

None of this means invalidation-based caching is never right — sometimes staleness genuinely isn’t acceptable, and building the more careful version is the actual job, not a shortcut to avoid. The point is that this should be a deliberate escalation made because bounded staleness was tried and found wanting, not a default reached for because precise invalidation sounds more rigorous. Ask first whether the data can tolerate being briefly out of date, and if the honest answer is yes, take the cache that’s correct by construction over the one that’s correct by careful discipline maintained indefinitely by everyone who ever touches the code.

This closes the caching thread where it started: a cache is a promise, and every promise has a cost proportional to how hard it is to keep. A short-TTL cache keeps an easy promise trivially. A precise-invalidation cache keeps a hard promise, and only as reliably as every future change respects it. Most systems are better served by the easy promise, kept completely, than by the hard promise, kept almost completely — because “almost” is exactly where every bug in this thread has lived.