Skip to main content
Case Studies

CDN Caching Isn't a Magic Switch: What Real Case Studies Teach Us

Most teams think adding a CDN instantly fixes performance. Real case studies show caching and load balancing require understanding headers, eviction, and algorithms.

Here's a misconception that costs teams real money and performance: just sticking a CDN in front of your origin and calling it a day will magically make everything fast. It won't. We've seen it time and again—teams expect a 95% cache hit ratio out of the box, but they're getting 20% because they didn't think about cache keys, headers, or what the load balancer is doing. In this article, we'll bust that myth and walk through the real decisions that make caching and load balancing work, based on case studies and the documentation we actually use.

Why doesn't my CDN cache my dynamic pages?

This is the first question we hear. You've put Cloudflare in front of your site, but every request still hits the origin. The answer: you haven't told the CDN what's cacheable. By default, Cloudflare caches certain status codes when no Cache-Control or Expires headers are present—200, 206, and 301 for 120 minutes, 302 and 303 for 20 minutes, 404 and 410 for 3 minutes—but everything else is considered dynamic and not cached (Cloudflare default cache behavior). If your pages return a 200 but you've set Cache-Control: no-store, the CDN won't store them. And if you're serving dynamic content—HTML that changes per user—you need to use the Vary header to split the cache key. For example, if you serve different content based on Accept-Encoding or Cookie, you must set Vary: Accept-Encoding so the CDN stores separate copies, otherwise it will serve the wrong variant to someone (MDN Vary).

What's the real difference between cache-aside and write-through?

We see teams overcomplicate this. The most common pattern is cache-aside, sometimes called lazy loading: check the cache first, on a miss fetch from the database, then write the result back to the cache (AWS caching overview). That's it. It's simple and works for most read-heavy workloads. But it has a failure mode: when the cache is empty, a thundering herd of requests can all miss and hit the database simultaneously. That's where a load balancer with the right algorithm helps, but also consider a cache lock. Nginx has proxy_cache_lock which lets only one request populate a new cache element while others wait (Nginx proxy module). That's a smart thing to enable if you see origin spikes on cache misses.

How do I choose between Redis and Memcached?

This isn't a religious war; it's about what your data looks like. Memcached is multithreaded and can use multiple cores, but it lacks advanced data structures. Redis supports lists, sets, sorted sets, hashes, bit arrays, hyperloglogs, and more, plus snapshots, replication, transactions, pub/sub, and Lua scripting (AWS ElastiCache Redis vs Memcached). If you need to do anything beyond simple key-value gets and sets, Redis is the clear winner. If you just need a simple, fast cache and you're on a multi-core box, Memcached's slab allocator is efficient—it divides memory into fixed-size slabs to reduce fragmentation (Memcached slab allocator). But we've seen teams outgrow Memcached quickly when they needed to store lists or do atomic increments. Redis's INCR command is a game-changer for counters—it's O(1) (Redis INCR). If you need that, don't fight it; go Redis.

Why is my cache hit ratio so low despite using a CDN?

First, check your cache keys. If you're not using the Vary header correctly, you might be caching multiple variants of the same resource, diluting your hit ratio. For example, if you serve different content based on Accept-Encoding or Cookie, you must set Vary: Accept-Encoding so the CDN stores separate copies, otherwise it will serve the wrong variant to someone (MDN Vary). Second, look at your eviction policy. In Redis, if you set maxmemory-policy to noeviction, the cache will start returning errors for writes when memory is full, which is terrible for a cache. You want allkeys-lru or allkeys-lfu—a cache is not storage, so evict something (AWS caching overview). Third, consider your load balancer's hashing. If you're using Nginx as a load balancer and you need sticky sessions or to improve cache locality, use the hash key consistent directive with the ketama method. That way, adding or removing a server only remaps a few keys, which helps maintain a higher cache hit ratio (Nginx upstream module).

Is stale-while-revalidate safe for my site?

Yes, and it's one of the best tools we have for perceived performance. RFC 5861 defines stale-while-revalidate, which lets a cache serve a stale response for up to a specified number of seconds while it revalidates in the background (RFC 5861). This is a lifesaver for content that changes infrequently but is expensive to generate. For example, you might set Cache-Control: max-age=60, stale-while-revalidate=600 so that after 60 seconds, a user can still get a slightly stale page while the CDN fetches a fresh one. Just be careful: if you have data that must be fresh, like a user's shopping cart, don't use it. But for a blog post or product page, it's a no-brainer.

How do I handle cache invalidation when I update content?

This is where most teams get burned. You update a product description, but the CDN keeps serving the old one. The most reliable way is to purge the cache. Cloudflare's purge by single-file instantly removes a cached resource from the CDN's stored assets across all data centers, so the next request gets the latest version (Cloudflare purge by single-file). That's a manual process, but you can automate it with a CMS webhook. Another approach is to use validators like ETag or Last-Modified. An ETag is an identifier for a specific version of a resource; when the content changes, the ETag changes, and the client can send a conditional request like If-None-Match to get a 304 Not Modified response if nothing changed (MDN ETag). This saves bandwidth and reduces load on your origin. Last-Modified is a fallback, but it's less accurate than an ETag (MDN Last-Modified). And remember, per RFC 9111, when a cache receives a non-error response to an unsafe method like POST or PUT, it must invalidate the stored target URI (RFC 9111). So if you update a resource via POST, the cache should drop it.

What's the most important thing to remember?

The single most important thing is to understand your cache keys. Every layer—browser, CDN, load balancer, application—has its own cache key, and if you don't configure them consistently, you'll get low hit ratios and stale data. Start with your HTTP headers: use Cache-Control with explicit max-age, and set Vary correctly. Then set your eviction policies and load balancer hashing to keep keys stable. And when in doubt, purge. The CDN is not a magic black box; it's a tool you control.

Sources

  • 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/
  • MDN (Vary) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
  • Nginx (upstream module) - https://nginx.org/en/docs/http/ngx_http_upstream_module.html
  • RFC 5861 (stale-while-revalidate) - https://www.rfc-editor.org/rfc/rfc5861.txt
  • AWS (caching overview) - https://aws.amazon.com/caching/

Share this article:

Comments (0)

No comments yet. Be the first to comment!