Skip to main content
Performance Tuning

Is Your Cache Making You Slower? Tune Your Cache-Control Headers First

You think adding Redis or a CDN will fix latency, but your cache headers are likely the real bottleneck. Here's how to tune Cache-Control and validators for maximum hit ratio.

You've added Redis, you've added a CDN, and your origin is still taking a beating. Here's the contrarian truth: your cache headers are probably the problem, not your infrastructure. Most teams bolt on a distributed cache or a load balancer and expect magic, but the HTTP cache layer—the one that sits in front of everything—is where the real wins live. Get your Cache-Control headers right, and you'll offload more traffic than any Redis cluster ever will.

Why Cache-Control Headers Trump Your Redis Cluster

Think about the layers: browser cache, CDN edge, load balancer, application cache, distributed cache, then the database. That's the typical stack (AWS caching overview). A CDN can absorb 70% or more of your origin requests and hit ratios commonly reach 80–95% (AWS caching overview). But that only happens if the CDN is allowed to cache your responses. If you're sending Cache-Control: no-store on every response—or worse, nothing at all—you're forcing every request through to your origin, and Redis becomes a very expensive way to serve the same static HTML over and over.

Here's the blunt advice: stop tuning your Redis eviction policy before you've fixed your Cache-Control headers. Redis is for the stuff that must be dynamic. HTTP caching is for everything else. And the HTTP layer is cheaper, faster, and closer to the user. A CDN edge in North America might be 55 cities deep (Cloudflare network), but a browser cache is literally on the user's device.

The Headers That Actually Move the Needle

Your goal is to maximize the number of requests that never reach your origin. That means you need to set Cache-Control correctly. The most important directive is max-age, which tells a cache how many seconds to keep the response fresh. For a static asset like an image or a versioned CSS file, you can set max-age=604800—that's 7 days (MDN Cache-Control). If you're not versioning your assets, you're asking for pain, but that's a separate rant.

But here's the nuance: max-age is measured from the moment the response is generated at the origin, not when it's received (MDN Cache-Control). So if your origin takes 2 seconds to generate a response, that's 2 seconds shaved off your freshness lifetime. And if you have a load balancer or CDN in between, that response might be sitting in a buffer for a while. The clock is ticking.

Now, what about s-maxage? That's the shared-cache override. Per RFC 9111, when s-maxage is present, a shared cache (like a CDN) must ignore max-age and Expires. So you can set a shorter max-age for the browser and a longer s-maxage for your CDN—that's a powerful tool for keeping your origin cold while still refreshing user-facing content frequently.

And don't forget stale-while-revalidate. This is a game-changer from RFC 5861: a cache can serve a stale response while it revalidates in the background. You tell the cache, "Hey, it's okay to serve this stale copy for up to 60 seconds while you check the origin." That's how you eliminate the latency spike on a cache miss. Set Cache-Control: max-age=60, stale-while-revalidate=60 and your users never wait for a revalidation.

Validators: Your Safety Net for Revalidation

When a cache does revalidate, it needs a way to ask the origin, "Has this changed?" That's where validators come in. ETag is the gold standard—a unique identifier for a specific version of a resource. If the content hasn't changed, the origin can return a 304 Not Modified, saving bandwidth (MDN ETag). Last-Modified is a fallback, less accurate, but better than nothing (MDN Last-Modified).

If you're not sending validators, every revalidation is a full 200 response. That's a wasted round trip. And if you're sending Last-Modified but your content changes every millisecond, you're going to have a bad time. Use ETags for anything that changes dynamically.

But here's the trap: if you're serving different content based on, say, the Accept-Encoding header, you need to tell the cache that. That's what Vary is for. If you don't set Vary: Accept-Encoding, a cache might serve a gzipped version to a client that doesn't support gzip—and that's a broken page. The Vary header is your cache key, and getting it wrong is like having a cache that returns the wrong data.

Putting It All Together: A Real-World Example

Let's say you run an e-commerce site. Your product detail pages are dynamic—prices change, stock changes—but the HTML is mostly the same for 5 minutes. Your static assets (images, JS, CSS) are versioned and immutable.

  • Static assets: Cache-Control: public, max-age=604800, immutable (the immutable directive tells the browser not to revalidate even on reload—MDN Cache-Control).
  • Product pages: Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60.
  • Cart or checkout: Cache-Control: no-store—never cache this.

With that, your CDN will serve product pages for 5 minutes, then revalidate in the background. Your origin sees a tiny fraction of the traffic. And your browser never revalidates static assets, so the site feels instant.

But wait—what about cache invalidation? If you change a product price, you don't want to wait 5 minutes for the CDN to serve stale data. That's where purge by single-file comes in. Cloudflare lets you instantly purge a single URL across all data centers (Cloudflare purge by single-file). You can trigger that from your admin panel when a price changes. Or, better yet, use a short s-maxage for content that changes often.

Quick tip: If you're using Vary: *, you're telling the cache that the response depends on everything—which effectively makes it uncacheable. Avoid it unless you really mean it.

The Bottom Line

Stop buying more Redis nodes. Start by auditing your HTTP cache headers. Set max-age aggressively on static assets, use s-maxage to differentiate browser vs. CDN caching, add stale-while-revalidate to smooth over revalidation, and always send strong ETags. That's the cheapest performance tuning you'll ever do. A CDN with the right headers can offload 70% or more of your origin traffic (AWS caching overview). Redis is for what's left.

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
  • 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/

Share this article:

Comments (0)

No comments yet. Be the first to comment!