A cache doesn't move data. It copies it. And the moment a copy exists, someone has to decide how long it's allowed to be wrong.
Caching is one of the most reflexively-reached-for performance tools in networked systems — memory instead of disk, a CDN edge node instead of the origin server, a browser's local cache instead of a round trip over the network. In every case the mechanism is identical: put a copy of some data somewhere closer and cheaper to read than the authoritative source, so future requests are answered faster and the original source does less work. What gets skipped over just as reflexively is that this copy is now a second version of the truth, sitting apart from the source that can still change out from under it. Every caching layer, without exception, has to answer one question explicitly: what happens when the source changes and the cached copy doesn't know yet?
Picture a CDN sitting between a client and an origin server. When a request arrives, the edge node checks whether it already holds a copy of the requested object. If it does — a cache hit — the request is satisfied entirely at the edge, and the origin server never even hears about it. If it doesn't — a cache miss — the request has to travel all the way to the origin, get processed there, and travel back, at which point the edge stores a copy for next time. Both paths return the same data. Only one of them costs a round trip to the source.
The moment a cache stores that copy, it has frozen a snapshot in time. If the underlying data at the origin changes afterward — a price update, a permission change, an edited page — the cached copy has no built-in way of knowing. It will happily keep answering requests with the old value until something forces the issue. Every caching system has to pick, explicitly or by default, one of three ways of handling that:
A time-to-live expires the entry after a fixed duration, whether or not the source actually changed. Simple, but blind to real change timing.
The source actively tells the cache "this entry is stale, drop or refresh it" the moment a change happens. Precise, but requires the source to know about every cache.
Before serving, the cache cheaply asks the source "has this changed since I cached it?" (e.g. ETag / If-Modified-Since) and only refetches the body if the answer is yes.
Caching aggressively — long TTLs, rarely checking back with the source — maximizes the speed and load-reduction benefit that made caching worth doing in the first place, but it means a stale value can sit in front of users for a long time after the real data changes. Caching conservatively — short TTLs, frequent validation checks — keeps data closer to current, but every one of those checks costs a round trip back to the source, eating into the exact latency and load savings caching was supposed to deliver. There is no TTL that eliminates the tradeoff. There's only a TTL tuned to how stale a given piece of data is allowed to be.
Incomplete at best. Caching always introduces the possibility of serving data that no longer matches the authoritative source, for some window of time — that's not an edge case, it's the defining property of what a cache is. The real engineering decision was never "cache or don't cache." It's choosing a TTL or invalidation strategy appropriate to how stale a given piece of data is actually allowed to be for the application in front of it. A product price on a listing page might tolerate a few minutes of staleness without anyone noticing. An account balance, an active session's permissions, or a security authorization check typically cannot tolerate any meaningful staleness at all — serving a stale "yes" to a permission check is a security bug, not a performance win. Treating caching as a free performance upgrade, without deciding on purpose how stale is acceptable, is a genuinely common real source of bugs— users who still see an old price after a sale ends, an employee who still has access after being removed from a group, or "ghost" data that reappears after being deleted because a cache never got the memo.
Explains why every caching layer — a CDN edge cache, an in-memory application cache, or a browser cache — trades reduced latency and origin load for the risk of serving data that no longer matches the authoritative source, and walks through the three main ways caching systems manage that risk: fixed TTL expiry, explicit invalidation, and cheap revalidation checks.
A cache stores a copy of data somewhere closer or cheaper to read than its authoritative source — in memory instead of on disk, at a CDN edge node instead of at the origin server, in a browser instead of re-fetched over the network. The purpose is always the same: reduce access latency for the requester and reduce repeated load on the source. The cost is always the same too: the copy is now a second, independent version of the truth that can drift out of sync with the source.
If the underlying data changes after it has been cached, the cached copy becomes stale — out of date relative to the source — until something updates it. This isn't a defect in a particular cache implementation; it's an inherent property of caching itself, because a cache by definition holds a copy taken at some point in the past. Every caching system therefore has to make an explicit or implicit choice: a time-to-live (TTL) that expires the entry after a fixed duration regardless of whether the source changed, invalidation where the source actively tells the cache an entry is now stale, or a validation check where the cache cheaply asks the source "has this changed?" before deciding whether to reuse the cached copy.
Caching aggressively — long TTLs, infrequent revalidation — maximizes the latency reduction and origin-load reduction that make caching worthwhile, at the cost of a longer window during which stale data can be served after the source changes. Caching conservatively — short TTLs, frequent validation — shrinks that staleness window, but each revalidation costs a round trip back to the source, eating into the very performance gain caching exists to provide. This tension is often summarized by Phil Karlton's remark that cache invalidation is one of the two genuinely hard problems in computer science: there is no setting that removes the tradeoff, only a setting tuned to how stale a specific piece of data is allowed to be.
The right answer is rarely a single sitewide TTL. Data that changes rarely and where staleness is low-stakes — a product description, a static image, a blog post — can tolerate hours or days of caching. Data that changes often or where staleness has real consequences — an account balance, current inventory count, a live security permission or authorization check — typically needs a very short TTL, mandatory revalidation, or explicit invalidation the moment the source changes, and in some cases (like an auth check) should not be cached at all. Picking the TTL is picking how wrong the system is allowed to be, for how long.
A TTL (time-to-live) expires a cached entry after a fixed duration automatically, with no communication from the source — simple, but blind to whether the source actually changed. Invalidation is the source (or an operator) explicitly telling the cache "this entry is now stale, discard or refresh it" the moment a real change happens — precise, but requires the source to track and notify every cache holding a copy. A validation check (e.g. HTTP conditional requests using ETag or If-Modified-Since) has the cache cheaply ask the source "has this changed since I cached it?" before deciding whether to keep serving the cached copy or refetch — it avoids blind expiry while still costing a lightweight round trip.
The line originates with programmer Phil Karlton's remark that there are only two hard problems in computer science: cache invalidation and naming things (plus off-by-one errors, in the joke version). It's hard because correctly invalidating a cache requires knowing, at the exact moment data changes, every place a stale copy might exist — across services, CDN edge nodes, browsers, and any downstream caches — and reliably notifying all of them without either missing one (leaving stale data live) or over-invalidating (erasing the performance benefit caching provided).
It reduces the maximum staleness window, but it does not make the system "more correct" for free — it trades away a proportional amount of the performance benefit, since a shorter TTL means far more cache misses and far more requests reaching the origin. For data where any staleness is unacceptable (e.g. checking whether a user is still authorized to perform an action), the right answer usually is not "a very short TTL" but "do not cache this at all, or use a strict invalidation event instead."
They are the same idea applied at different points in the path. A browser cache stores a copy on the requesting device itself, avoiding the network entirely on a hit. A CDN edge cache stores a copy at a server geographically or topologically close to the requester, avoiding the trip all the way to the origin but still requiring a request over the local network to the edge node. Both are governed by the same HTTP caching headers (e.g. Cache-Control, ETag) and face the identical staleness tradeoff — they just sit at different layers of the same request path.
Common real-world symptoms include users still seeing an old price after it changes, a removed employee retaining access because a stale permissions cache hadn't expired yet, "ghost" content reappearing after deletion because a cache never received an invalidation, and inconsistent behavior between users depending on which cache node or browser cache state happened to serve them. Nearly all of these trace back to the same root cause: a TTL or invalidation strategy that assumed a piece of data could tolerate more staleness than it actually could.
Try our Enterprise IT Networks Studio
More calculators, simulators, and guides for this discipline.