The 120-Minute Default That's Costing You
Here's a number that should make you uncomfortable: by default, Cloudflare caches certain HTTP status codes for 120 minutes when no cache-control or expires headers are present (Cloudflare, default cache behavior). That means your origin server might be serving stale content for two hours simply because you didn't tell the CDN what to do. I've seen teams chase cache hit ratios and buy bigger Redis clusters while ignoring the most fundamental caching decision: what you tell shared caches about freshness. Stop tuning your eviction policy before you've mastered your headers.
The Thesis: Headers Beat Hardware
My position is simple: before you spend a dollar on a new cache layer or a fancier load balancer, you need to get your HTTP cache headers right. The Cache-Control header is the single most powerful caching tool at your disposal, and it's free. It's the difference between a CDN that works for you and one that works against you. The fact that many teams default to no-store on everything or, worse, leave it to the CDN's whims, is a crime against performance.
Consider the layered caching architecture: browser cache, CDN edge, load balancer proxy, application cache, distributed cache, then database (AWS, caching overview). Each layer respects Cache-Control differently. If you set a sensible max-age, you let the browser cache absorb repeat visits, the CDN absorb regional traffic, and your origin only sees the misses. That's the dream. But most teams I see either set max-age too low, or they set no-store everywhere because they're afraid of staleness. Both are lazy.
The Right Way to Think About Freshness
Let's get concrete. The max-age directive is measured in seconds from when the response is generated on the origin, not when it's received (MDN, Cache-Control). So if you set max-age=604800, that's a week of freshness. For a static asset like a logo, that's perfect. For a product price, maybe not. The key is to match freshness to the actual tolerance for staleness. And don't forget s-maxage: it's the shared-cache counterpart to max-age, and per RFC 9111, if s-maxage is present, shared caches must ignore Expires (RFC 9111). So you can have a long s-maxage for CDN caching and a shorter max-age for the browser, or vice versa.
And here's where it gets interesting: stale-while-revalidate. RFC 5861 defines this extension, which lets a cache serve a stale response for up to a specified number of seconds while revalidating in the background (RFC 5861). This is a game-changer for dynamic content. Instead of a hard no-store, you can say, 'Serve this stale for 60 seconds while you check the origin.' Users get instant responses, and your origin gets fewer hits. It's a graceful degradation that most teams don't use because they don't know it exists.
The Counter-Argument: What About Dynamic Data?
I can hear the objection already: 'But my API returns user-specific data! I can't cache that.' And you're right, to a point. That's what the Vary header is for. Vary tells the cache to store separate responses based on request headers, like Accept-Encoding or Authorization (MDN, Vary). So you can cache a response per user, as long as you include the right Vary headers. It's not a one-size-fits-all, but it's a start. The real problem is that most teams use no-store as a blunt instrument, throwing away caching opportunities for data that could be safely cached with a little nuance.
Also, consider the invalidation angle. RFC 9111 states that unsafe methods like PUT, POST, or DELETE must invalidate the cached target URI when they return a non-error status (RFC 9111). So you can allow caching on a GET endpoint, and when a user updates their profile via POST, the cache for that URI is automatically invalidated. That's the standard behavior. Combine that with CDN purge-by-single-file, which Cloudflare offers, and you have a robust invalidation story (Cloudflare, purge by single-file). The tools are there. You just have to use them.
A Comparison: Header Strategies That Work
Let me lay out a few common strategies and when they shine. I'll compare them based on freshness control, invalidation, and implementation effort.
| Strategy | Best For | Key Directives | Freshness Control |
|---|---|---|---|
| Static Asset Caching | Images, CSS, JS | max-age=604800, immutable | High (weeks) |
| Freshness with Revalidation | Dynamic HTML, API responses | no-cache, ETag, Last-Modified | Medium (revalidate each time) |
| Stale-While-Revalidate | High-traffic, slightly stale OK | stale-while-revalidate=60 | Medium (serve stale during revalidation) |
| No-Store | Personal data, admin APIs | no-store | None (always fresh) |
Each strategy has its place. Static assets should be cached with a long max-age and immutable to avoid revalidation. Dynamic content can use no-cache, which allows storage but requires revalidation before reuse (MDN, Cache-Control). That's a safe middle ground. And for truly sensitive data, no-store is fine, but don't default to it for everything.
My Recommendation: Start With Your Headers
Here's what I'd do tomorrow: audit your existing responses. Look at what Cloudflare is caching by default, and you'll probably find a mess. Then set explicit Cache-Control headers on every response. For static assets, use max-age=604800 and immutable. For dynamic content, use no-cache with ETags for efficient revalidation. ETags are an identifier for a specific version of a resource, and if the content hasn't changed, the server doesn't have to resend the full response (MDN, ETag). Last-Modified is a fallback, but ETags are more accurate (MDN, Last-Modified).
And consider stale-while-revalidate for your highest-traffic endpoints. It's a win-win: your users get faster responses, and your origin gets a breather. The implementation is just a header change, and the payoff is huge. I've seen CDN hit ratios reach 80-95% when headers are tuned correctly (AWS, caching overview). That's not a pipe dream; it's a realistic outcome.
So before you buy another Redis node or switch load balancers, fix your headers. It's the cheapest performance win you'll ever get.
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
- 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/
- AWS (caching overview) - https://aws.amazon.com/caching/
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!