Skip to main content
Caching Strategies

Caching Isn't a Set-and-Forget Toggle: Rethink Your Edge Strategy

Stop treating caching as a one-size-fits-all switch. We debunk the 'just set max-age' myth and show you how to layer caches, handle stale-while-revalidate, and pick eviction policies that actually fit.

You've heard it a thousand times: "Just slap a CDN in front and set a long max-age." That advice is lazy, and it's why your cache hit ratio is stuck at 60% while your origin still screams. In the real world, caching is a system of trade-offs, not a toggle. It's about knowing which layer to trust, when to revalidate, and how to admit that you sometimes need to serve stale data to keep the site alive. Let's bust the myths and get practical.

Isn't a CDN just a cache that makes everything faster?

No, and that misconception causes real pain. A CDN is a geographically distributed group of servers that speeds up delivery by caching content closer to users (Cloudflare, what is a CDN). But it's not magic. By default, Cloudflare only caches certain status codes when no cache-control headers are present: 200/206/301 for 120 minutes, 302/303 for 20 minutes, and 404/410 for a mere 3 minutes (Cloudflare, default cache behavior). If you're serving dynamic HTML without headers, you're getting almost nothing. And even with headers, CDNs have file size limits—Free through Business plans cap at 512 MB, Enterprise at 5 GB (Cloudflare, default cache behavior). If you're trying to cache a 1 GB video on a Business plan, it won't happen. Know your CDN's defaults before you blame the origin.

Should I set max-age to a year for everything?

Only if you want to frustrate your users and yourself. The max-age=N directive keeps a response fresh for N seconds after it was generated on the origin server—not after it was received (MDN, Cache-Control). So if you set max-age=604800 (7 days) on a page that changes daily, you're serving stale content for up to a week. And here's the kicker: when a response carries max-age, per RFC 9111, recipients MUST ignore the Expires header (RFC 9111). So that old Expires fallback you're relying on? Useless. The real art is choosing a TTL that matches your content's actual volatility. For truly static assets—images, CSS, JS with fingerprints—a long max-age is fine. For HTML that changes often, you need revalidation, not a long TTL.

Isn't stale-while-revalidate just a hack for slow origins?

It's not a hack; it's a lifeline. RFC 5861 defines stale-while-revalidate as a Cache-Control extension that allows a cache to serve a response that is stale for up to a specified number of seconds while it revalidates in the background (RFC 5861). This means your users get instant responses—even if slightly old—while your origin updates the cache. For example, a news article that's been up for 10 minutes can be served stale for 5 seconds while the CDN fetches the latest version. This is how you keep a 95% hit ratio without sacrificing freshness. The key is to set a reasonable window—not 3600 seconds—for content that changes frequently. It's a tool for graceful degradation, not an excuse to ignore updates.

What about cache invalidation? Can't I just purge everything?

Purge by single-file is your scalpel; purge-by-tag is your machete. Cloudflare's purge by single-file instantly removes a cached resource across all data centers, so the next request gets the latest version (Cloudflare, purge by single-file). But if you purge an entire directory or use a wildcard, you're nuking hundreds of files and causing a thundering herd of requests to your origin. The smarter approach is to use cache keys that include version identifiers—like /v2/js/app.js—so you never need to purge; the URL changes and the old one expires naturally. Invalidation is a last resort, not a strategy.

Is ETag better than Last-Modified for revalidation?

Yes, and here's why: ETag is an identifier for a specific version of a resource, and when content hasn't changed, the server doesn't need to resend the full response, saving bandwidth (MDN, ETag). Last-Modified is less accurate because it's just a timestamp and can be fooled by a file that's touched but unchanged (MDN, Last-Modified). For API responses that change subtly, ETags are essential. But here's a nuance: if you're using a CDN, you often don't control the ETag generation—your origin does. And if your origin doesn't emit an ETag, you're stuck with Last-Modified. So, implement ETags on your origin—it's a small change with a big payoff.

What's the deal with Vary? Should I use it?

Vary is a double-edged sword. It lists the parts of the request that influenced the response, so caches store separate copies for different header values (MDN, Vary). For example, if you serve different content based on Accept-Encoding, you must include Vary: Accept-Encoding or you'll serve compressed content to a client that can't decompress it. But Vary: * makes a response uncacheable—it's a cache killer. The problem is that many developers use Vary incorrectly, for headers like User-Agent, which fragments your cache into a million pieces. Use Vary sparingly—only for headers that truly change the representation, like Accept-Language or Accept-Encoding.

Should I use Redis or Memcached for my application cache?

If you need more than a simple key-value store, choose Redis. Memcached is multithreaded and can use multiple cores, but it lacks advanced data structures (AWS, ElastiCache Redis vs Memcached). Redis supports lists, sets, sorted sets, hashes, and more, plus persistence, pub/sub, and Lua scripting (AWS, ElastiCache Redis vs Memcached). In practice, Redis is the default for any non-trivial caching need. But don't ignore eviction policies. Redis uses LRU by default, but you can set maxmemory-policy to allkeys-lru, allkeys-lfu, volatile-ttl, and others (Redis, eviction). For a cache, allkeys-lru is recommended because a cache is not storage—you want it to evict anything when memory is full, not just keys with a TTL (AWS, caching overview). If you have a key that's rarely accessed but has a long TTL, volatile-lru might keep it forever, which is wrong.

What about load balancer caching? Is that a thing?

Yes, a load balancer can cache, but don't confuse it with a CDN. Nginx, for example, can cache proxied responses using proxy_cache_path and proxy_cache_valid directives (Nginx, proxy module). This is useful when you have multiple app servers behind a single nginx instance. But it's not a substitute for a CDN—it's an extra layer that sits closer to your origin. In a layered architecture, you have browser cache, CDN edge cache, load balancer/proxy cache, application cache, and then the database (AWS, caching overview). The load balancer cache is for the stuff that slips past the CDN—requests that aren't cached at the edge, like authenticated responses. But be careful: if you cache too much at the LB, you'll have stale data and a harder time invalidating. Use it for specific, low-churn endpoints.

Quick tip: Always set a short stale-while-revalidate window (e.g., 30 seconds) on your API responses to absorb spikes without adding latency.

What I'd actually do

If you're building a new service today, I'd start with a three-layer cache strategy. First, set proper Cache-Control headers with max-age, s-maxage, and stale-while-revalidate on all your static assets and public API responses. Use ETags for revalidation. Second, put a CDN like Cloudflare in front, but don't rely on its defaults—explicitly configure TTLs for each content type. For dynamic content, use the CDN's tiered cache (Cloudflare's Tiered Cache, which divides data centers into lower and upper tiers, so only the upper tier can contact your origin) to reduce origin load (Cloudflare, tiered cache). Third, for your application cache, use Redis with allkeys-lru eviction and a maxmemory that fits your workload. For load balancing, use Nginx or HAProxy, but keep the LB cache off unless you really need it—it's an extra moving part. And finally, test with real traffic. CDN hit ratios commonly reach 80% to 95% (AWS, caching overview). If you're below 80%, you're doing something wrong. Go fix it.

Sources

  • AWS (caching overview) - https://aws.amazon.com/caching/
  • 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/
  • Cloudflare (tiered cache) - https://developers.cloudflare.com/cache/how-to/tiered-cache/
  • MDN (Cache-Control) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
  • Nginx (proxy module) - https://nginx.org/en/docs/http/ngx_http_proxy_module.html

Share this article:

Comments (0)

No comments yet. Be the first to comment!