1 Overview
The question of how long a cache should live sounds like it has a single numeric answer, and almost every team eventually asks for one. In practice the answer is a range, and the range is derived from how often the underlying data actually changes, how expensive it is to recompute, and how bad it is to serve a stale value. A cache entry's lifetime is a policy decision about acceptable staleness, not a universal constant you can copy from a configuration example.
It helps to separate two things people usually blur together. The first is the freshness requirement of the data itself: how long after a change must readers see that change. The second is the economics of the cache: how much origin work, latency, and cost you avoid by serving a stored copy. A TTL is the dial that trades one against the other. Set it too short and the cache barely helps; set it too long and you serve wrong answers confidently.
This article treats TTLs as an engineering parameter to be reasoned about and measured, not a magic number. The guidance applies to shared HTTP response caches and to key-value object caches alike, because the underlying mechanism, a stored value with an expiry, is the same. What differs is the cost of being wrong and the tools available to correct it.
2 Where the common advice breaks down
The most common advice is a fixed default, often something like five minutes or an hour, applied uniformly across every key and every endpoint. That advice survives because it is safe on average and requires no thought. It breaks down the moment your traffic is not uniform. A single TTL across a catalog of mostly static items and a handful of fast-moving items forces you to pick the worst case, so you either over-cache the volatile items or under-cache the stable ones.
A second failure mode is treating TTL as the invalidation strategy. If the only thing that removes a stale value is expiry, then your freshness is bounded by the TTL and nothing else, which is a weak guarantee. Teams that rely on this end up shortening TTLs to compensate, which quietly destroys their hit rate and pushes load back to the origin. The TTL becomes a bandage over a missing invalidation path.
A third problem is ignoring key cardinality. Advice framed around a small set of popular objects does not transfer to a cache holding millions of rarely requested keys. With high cardinality and low per-key frequency, entries may expire before they are ever read again, so the cache adds memory pressure and eviction churn without improving hit rate. The right TTL depends on whether a key is likely to be requested again within its lifetime, which is a property of your traffic, not of the data alone.
3 How the mechanism actually works
A cache stores a value under a key along with an expiry timestamp. On a read, the cache checks whether the entry exists and whether it is still within its lifetime. If both hold, it returns the stored value, which is a hit. If the entry is missing or expired, the request falls through to the origin or the compute path, and the result is typically written back with a fresh expiry, which is a miss followed by a fill. Every miss costs the full price of producing the value; every hit costs only the lookup and transfer.
This means the hit rate is not a fixed property of the cache. It emerges from the interaction between request patterns and TTL. For a given key, the probability that a request arrives before expiry depends on how frequently that key is requested relative to its lifetime. Frequently requested keys with modest TTLs hit often. Rarely requested keys with the same TTL may expire between requests and never hit at all. Raising the TTL increases the chance that the next request finds a live entry, but it also increases the chance that the entry is outdated.
Expiry itself comes in more than one flavor. Some caches remove an entry exactly at expiry; others keep it and treat it as stale, allowing a stale-while-revalidate pattern where one request refreshes the value while others continue to be served the old one. That distinction matters because it changes the latency profile of a miss. A hard expiry exposes every refresh to a full origin round trip, while a soft, revalidating expiry can absorb that cost in the background at the price of briefly serving known-stale data.
4 Trade-offs you are really making
The central trade-off is freshness against efficiency. A longer TTL buys a higher hit rate, less origin load, lower average latency, and lower cost, because more requests are answered from stored copies. It also widens the window during which a change made at the origin is invisible to readers. Whether that window is acceptable depends entirely on the consequence of staleness: a stale product description is a minor annoyance, while a stale permission check or account balance is a correctness bug.
A second trade-off is between TTL and invalidation complexity. You can keep TTLs short and rely on expiry alone, which is simple but expensive. Or you can keep TTLs long and add explicit invalidation on write, which is efficient but requires every write path to know which keys to purge. The second approach is more work and more failure-prone, because a missed purge leaves stale data until the TTL eventually rescues you. That is why long TTLs and explicit invalidation usually belong together, with the TTL acting as a backstop rather than the primary control.
A third trade-off is memory and eviction. Longer TTLs keep entries resident longer, which consumes capacity and can evict other useful entries under a size-bounded cache. If eviction pressure is high, a long TTL may not even deliver the hit rate you expect, because entries are removed by capacity before they expire. TTL policy and cache sizing are therefore coupled decisions, not independent knobs.
5 Implementing it correctly
Start by classifying your data by change frequency and staleness tolerance rather than by endpoint. Stable, expensive-to-compute, widely shared values can carry long TTLs. Volatile, per-user, or correctness-sensitive values should carry short TTLs or be excluded from shared caching entirely. Apply these classes as distinct policies so that one fast-moving object does not drag the TTL of everything else down with it.
Next, make invalidation explicit wherever correctness matters. On a write, purge or overwrite the affected keys, or change the key itself by embedding a version or content hash so the old entry becomes unreachable. Versioned keys are attractive because they avoid purge races and make stale entries harmless, at the cost of leaving old entries to expire naturally. Whatever you choose, set the TTL as a safety net that bounds how long a missed invalidation can persist, not as the mechanism you depend on.
Finally, add jitter to expiries so that large groups of entries do not expire simultaneously and stampede the origin. A common pattern is to spread expiry over a window around the target lifetime. Also consider stale-while-revalidate semantics where the cache supports them, so a single refresh does not block every concurrent reader. Document the chosen TTL per data class and the reason for it, because the next engineer to touch the configuration needs to know which values are load-bearing and which are arbitrary.
6 Measuring whether it helped
The first metric is hit rate, but it must be measured per key class, not globally. A high overall hit rate can hide a class of keys that never hits, and a low overall rate can be dominated by a small set of expensive misses. Break the measurement down by the classes you defined, and watch how hit rate responds as you adjust TTL. If raising the TTL does not move the hit rate, the bottleneck is key cardinality or capacity, not lifetime.
The second metric is origin load, expressed as request rate and compute time reaching the backend. This is the cost the cache exists to remove, so it is the most honest measure of benefit. Pair it with latency at the client, ideally as a distribution rather than an average, because caching changes the shape of the tail as well as the middle. A cache that improves the median but worsens the tail under refresh stampedes is not obviously a win.
The third metric is staleness, which is harder to observe directly. Track how often a read returns a value older than the acceptable window, or instrument the gap between a write and the first read that reflects it. If you cannot measure staleness, you cannot claim your TTL is safe. Treat any TTL change as an experiment: adjust one class, hold the rest constant, and compare hit rate, origin load, latency, and staleness over a period long enough to cover normal traffic variation.
7 Frequently asked questions
Is there a single correct TTL? No. The correct TTL is the longest lifetime that keeps staleness within what your data and users can tolerate, given how often the data changes and how expensive it is to recompute. It is a per-class decision derived from freshness requirements and traffic shape, not a constant.
Should I use a short TTL or explicit invalidation? Use both when correctness matters. Explicit invalidation keeps data fresh at the moment of change, and a moderate TTL bounds the damage from a missed purge. Relying on expiry alone forces short TTLs and sacrifices hit rate; relying on invalidation alone leaves no backstop for bugs.
Does a longer TTL always mean a better hit rate? Not necessarily. If entries are evicted by capacity before they expire, or if keys are requested so rarely that they expire between reads, extending the TTL changes little. In those cases the limiting factor is cache size or key cardinality, and TTL tuning will not fix it.
How do I handle data that changes unpredictably? Separate it from stable data and give it a short TTL, or exclude it from shared caching and cache it only where the staleness cost is acceptable. You can also use versioned keys so that a change produces a new key instantly, leaving the old entry to expire harmlessly.
What about per-user data? Per-user values have high key cardinality and often low reuse, so they tend to hit poorly in shared caches and raise privacy concerns. Cache them only with short TTLs, clear scoping, and a clear understanding of the staleness cost, or not at all.