The Question: How Do You Pick a Cache TTL?
Imagine you're the engineer on call at 2 a.m. The origin server is struggling under a wave of traffic, and your team's knee-jerk fix is to "turn up the cache." You log into the CDN, type 604800 into the TTL field, and go back to sleep. But the next morning, your product manager is furious because price changes you pushed at 9 a.m. didn't show up for customers until 9 a.m. the next day. We've all been there. The real question isn't "should we cache?" — it's how long should a response stay fresh before we force a check with the origin? That single number, the freshness lifetime, determines a huge part of your performance, your origin load, and your users' perception of correctness.
The answer, as with most things in caching, is "it depends," but not in the wishy-washy sense. There are concrete trade-offs, and we can reason through them with the tools HTTP already gives us. The good news: you don't have to choose between a short TTL (safe but less effective) and a long TTL (dangerous but fast). A max-age of a few minutes, combined with revalidation and a background-refresh trick, often beats a naive 24-hour cache. Let's walk through the decision.
What Does max-age Actually Mean?
The core of any cache decision is the Cache-Control header. When your origin sends Cache-Control: max-age=604800, it's telling every cache — browser, CDN, or load balancer — that the response is fresh for seven days after it was generated on the origin (MDN (Cache-Control)). That's a long time. But that's only the first rule in a hierarchy: a shared cache will also look at s-maxage before max-age, and if neither is present, it may fall back to Expires or even a heuristic based on Last-Modified (RFC 9111 (HTTP Caching)). For most of us, max-age is the lever we pull.
Here's the trap: a long max-age means the cache won't even ask the origin if the content changed until that timer expires. If you set max-age=604800 for a product price, you are literally telling every cache in the world to serve that old price for a week. That's a correctness bug, and it's why many teams default to a much shorter freshness — think 60 seconds for HTML, or even 0 for anything that changes unpredictably. The trick is to decouple freshness from validity: you can keep a response in the cache for a long time, but you need to revalidate it often.
Revalidation: Your Safety Valve
HTTP gives us validators so a cache can ask the origin, "Has this changed?" without downloading the whole body. An ETag is a version identifier — if the content hasn't changed, the server can reply with a 304 Not Modified, saving bandwidth (MDN (ETag)). Last-Modified is a weaker fallback based on timestamps (MDN (Last-Modified)). When you combine a short max-age with a validator, you get the best of both worlds: the cache serves instantly while fresh, and when it goes stale, it does a cheap conditional request.
But here's the nuance: even a 304 requires a round trip to the origin. If you have a global audience and your origin is in one region, that round trip can be 100–200 ms. Do you want every user to pay that on every request? Probably not for a static image. That's where stale-while-revalidate comes in. This extension (defined in RFC 5861) lets a cache serve a stale response for up to a specified number of seconds while it revalidates in the background (RFC 5861 (stale-while-revalidate)). So you can set Cache-Control: max-age=60, stale-while-revalidate=3600. The first user after 60 seconds gets the stale copy instantly, and the cache refreshes in the background so the next user gets a fresh one. That single header can cut origin load dramatically without adding latency for your users.
Let's put numbers on it. Suppose you have a product page that changes at most once an hour. If you set max-age=3600, a user at minute 59 gets a fresh copy, but one at minute 61 gets a stale copy for up to an hour. That's bad. Instead, set max-age=60, stale-while-revalidate=3600. Now no user ever sees content older than 61 seconds, and your origin only gets hit once per hour per cache — not once per minute. That's the kind of win that shows up in your origin CPU graph.
What About CDN Defaults and Purge?
If you're using a CDN, you're not starting from a blank slate. Cloudflare, for example, has default cache behavior that kicks in when no Cache-Control or Expires header is present: it caches 200/206/301 responses for 120 minutes, 302/303 for 20 minutes, and 404/410 for 3 minutes (Cloudflare (default cache behavior)). That's a reasonable safety net, but it's a blunt instrument. The moment you send your own Cache-Control, you override those defaults — which is what you want, because your TTL should reflect your content's change frequency, not a generic guess.
Still, even with a sane TTL, you'll eventually need to invalidate something early — a breaking news story, a price error, a security fix. That's where purge comes in. Cloudflare's single-file purge removes a cached asset from every data center instantly (Cloudflare (purge by single-file)). So the workflow is: set a short max-age for dynamic content, and use purge as an emergency override. But purge is a manual operation — it doesn't scale to every URL. That's why the best practice is to keep TTLs short enough that you don't need purge for routine updates.
Here's a concrete scenario: an e-commerce site with 10,000 product pages, each with a price that changes at most a few times a day. If you set max-age=86400, you'll have a lot of stale prices and a lot of purging. If you set max-age=60, stale-while-revalidate=3600, your origin sees about 10,000 requests per hour (one per page per hour) instead of 10,000 per minute. That's a 60x reduction in origin load, and users never see a price older than a minute. That's a trade-off most teams would take.
The Bottom Line
Stop treating TTL as a single number you set and forget. The right move is to start with a short max-age — 60 seconds for HTML, or even 0 if you can't tolerate staleness — and add stale-while-revalidate to keep your origin load low. Use validators (ETag or Last-Modified) so revalidation is cheap. And reserve long TTLs for truly immutable assets like images and CSS, which can safely live for a week (max-age=604800) because they never change. For everything else, a short freshness plus background revalidation is the single best move you can make.
In the end, the cache is a tool for trading correctness for speed. The trick is to make the trade-off explicit, not accidental. Pick a TTL that matches your content's actual change rate, not your fear of origin load. Your users will thank you, and so will your origin server.
Sources
- MDN (Cache-Control) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
- RFC 9111 (HTTP Caching) - https://httpwg.org/specs/rfc9111.html
- RFC 5861 (stale-while-revalidate) - https://www.rfc-editor.org/rfc/rfc5861.txt
- MDN (ETag) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
- MDN (Last-Modified) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
- Cloudflare (default cache behavior) - https://developers.cloudflare.com/cache/concepts/default-cache-behavior/
- Cloudflare (purge by single-file) - https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-single-file/
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!