Skip to main content
Case Studies

Stop Blaming the CDN: A Case-Study Guide to Cache Invalidation and Load Balancing

You think your CDN is broken? It's probably your cache headers. This blunt, practical guide walks you through real-world cache and load balancer fixes, from stale-while-revalidate to consistent hashing.

Everyone blames the CDN when a site crawls. But I've seen more outages caused by a misconfigured Cache-Control header than by any network failure. Here's the contrarian truth: your CDN is probably doing its job—you're just asking it to do the impossible. This article is for the engineer who's tried the obvious fixes and still sees cache misses piling up. I'll walk you through the exact steps I take when a client's "CDN problem" turns out to be a cache-header or load-balancer issue.

1. Own Your Cache Keys Before You Touch the CDN

You can add a hundred edge nodes, but if your cache key is wrong, you're just distributing a mess. The single most overlooked lever is the Vary header. It tells the cache which request headers actually change the response. Get this wrong and you'll serve the wrong content to the wrong user—or, worse, you'll kill your hit rate by fragmenting the cache across a thousand variants. (MDN)

Start by auditing your responses. If you serve gzip or Brotli, the Vary: Accept-Encoding header is non-negotiable. If you personalize content by cookie or language, add those headers to Vary. But here's the blunt advice: less is more. Every header you add to Vary splits your cache into separate buckets. That's a direct hit to your hit ratio.

2. Use stale-while-revalidate—It's Your Best Friend

Most teams set max-age too low because they're terrified of serving stale data. That's a rookie move. The stale-while-revalidate directive (RFC 5861) lets you serve a stale response for up to N seconds while your origin refreshes it in the background. This is the single biggest win for perceived performance. For a product page that changes rarely, set max-age=60, stale-while-revalidate=300. Your users get instant loads, and your origin gets a trickle of traffic instead of a tsunami.

What can go wrong? If you set stale-while-revalidate too high on a page that really does change every second (like a stock ticker), you'll serve outdated numbers and your users will notice. Use it for content that's "mostly static"—not for real-time data.

3. Load Balancers Are NOT All the Same—Choose by Traffic Pattern

Round robin is the default on every load balancer, but it's the worst choice for anything beyond stateless, short-lived requests. If you have long-lived WebSocket connections or a database behind the balancer, use leastconn (HAProxy) or least_conn (nginx). The docs are clear: least connections is recommended for long sessions like LDAP or SQL, and round robin is for short HTTP requests. (HAProxy, Nginx)

For session persistence, don't rely on sticky cookies unless you absolutely must. Instead, use IP hash or consistent hashing. Nginx's hash directive with the consistent parameter uses ketama consistent hashing, which means when you add or remove a server, only a few keys remap. That's huge for cache hit ratios in a load-balanced cache tier. (Nginx)

4. Cache Invalidation Is Hard—So Make It Easy

We all know the pain of purging a CDN. But here's the thing: you should rarely need to purge manually. The HTTP spec has your back: when a client sends a non-safe method like POST, PUT, or DELETE, any cache must invalidate the stored URI. (RFC 9111) That's the law, and it works—as long as your origin actually sends those methods and returns a non-error status. So stop building custom purge endpoints and just make your API do the right thing.

For the times you do need a manual purge, make it surgical. Cloudflare's single-file purge instantly removes one asset across all data centers. (Cloudflare) That's the right tool for a hotfix. Avoid the nuclear option of purging everything unless you're prepared for a stampede of origin requests.

5. Redis Is Not a Database—Treat It Like a Cache

I've seen teams store critical user data in Redis and then wonder why they lose it on restart. Redis is an in-memory cache, not durable storage. If you're using it for caching, you should be fine with the default allkeys-lru eviction policy (AWS) and no persistence. If you need durability, you're using the wrong tool.

But there's a middle ground: Redis can be a pub/sub bus or a rate limiter. The INCR command is perfect for a sliding window rate limiter—it's atomic and O(1). (Redis) And if you need multi-key operations that are atomic, use Lua scripting instead of transactions. Scripts run atomically and block the server, which is exactly what you want for a complex update. (Redis)

Sources

  • MDN Cache-Control - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
  • RFC 5861 - https://www.rfc-editor.org/rfc/rfc5861.txt
  • RFC 9111 - https://httpwg.org/specs/rfc9111.html
  • Nginx HTTP load balancing - https://nginx.org/en/docs/http/load_balancing.html
  • Nginx upstream module - https://nginx.org/en/docs/http/ngx_http_upstream_module.html
  • HAProxy configuration manual - https://docs.haproxy.org/3.4/configuration.html
  • Cloudflare purge by single-file - https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-single-file/
  • Redis eviction - https://redis.io/docs/latest/develop/reference/eviction/

Share this article:

Comments (0)

No comments yet. Be the first to comment!