You type: “Why is my cache hit ratio so low?” I’ve seen this question ruin weekends. The standard answer is to throw Redis at it, tune eviction policies, and hope. But that’s backwards. The cache layer that most teams ignore—the one that gives you 80–95% hit ratios almost for free—is the one sitting right at the edge of your network, not in your application cluster.
Start at the Edge, Not the App
I’m tired of seeing engineers reach for a distributed cache before they’ve configured a single HTTP header. Here’s the deal: a CDN is a geographically distributed group of servers that speeds up delivery by caching content closer to users (Cloudflare, what is a CDN). And the numbers are not subtle—CDN hit ratios commonly reach 80% to 95%, and CDNs can absorb 70% or more of origin requests (AWS caching overview). That’s not a marginal win; that’s an order-of-magnitude reduction in the load your origin has to handle. Before you even think about Redis, you should be asking: “Can this response be cached at the edge?” If yes, cache it there. If no, figure out why.
HTTP Headers Are the Real Cache Control
Your cache is only as smart as the headers you send. The Cache-Control header is the master switch, and it supports directives like max-age, no-store, private, and public (MDN Cache-Control). But here’s where people screw up: they set max-age and forget that it’s measured from the moment the response is generated on the origin, not when it’s received (MDN Cache-Control). That subtlety matters for dynamic content. And if you’re still using Expires alongside Cache-Control, stop—RFC 9111 says that when max-age is present, recipients MUST ignore Expires (RFC 9111).
The more interesting directive is stale-while-revalidate, defined in RFC 5861. It lets a cache serve a stale response for up to a specified number of seconds while it revalidates in the background (RFC 5861). I’m a fan because it turns the worst-case scenario—a cache miss that thunders to your origin—into a non-event. Set it on your product pages, and you’ll see your origin latency graph flatten.
Validators: The Unsung Heroes of Bandwidth
ETags are the precise tool for revalidation. An ETag identifies 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 a fallback, less accurate, but still useful (MDN Last-Modified). I’ll say this bluntly: if you’re not using ETags on your API responses, you’re wasting bytes and time. Conditional requests with If-None-Match turn a 200 with a full body into a 304 with no body. That’s not a minor optimization; it’s the difference between a snappy mobile app and one that feels like dial-up.
But here’s the kicker: you can’t just slap an ETag on everything. You have to think about the cache key. The Vary header tells caches to store separate copies based on request headers like Accept-Encoding or User-Agent (MDN Vary). Ignore Vary, and you’ll serve a gzipped version to a client that can’t decode it, or a mobile layout to a desktop browser. That’s not a cache bug; it’s a cache key bug.
The Counter-Argument: “But My Data Is Dynamic!”
I hear you. “Our content is personalized, so we can’t cache at the edge.” That’s the most overused excuse in the book. Yes, if you use Vary: *, you’re telling caches the response is uncacheable (MDN Vary). But most dynamic pages have a static shell—the navigation, the footer, the product images. Cache those components with a long max-age and an ETag. And for the truly dynamic bits, use a CDN edge that supports tiered caching. Cloudflare’s Tiered Cache, for instance, divides data centers into lower and upper tiers. On a miss, the lower-tier data center asks an upper-tier, and only the upper tier can contact your origin (Cloudflare tiered cache). That reduces origin load and creates a cache hierarchy that behaves like a well-oiled machine.
What about cache invalidation? People panic, “How do I purge when I update a product?” Cloudflare’s purge by single-file instantly removes a cached resource across all data centers (Cloudflare purge by single-file). That’s a sledgehammer, but you don’t need it often if you use versioned filenames—app-123.js rather than app.js—and let max-age expire naturally. The key is to design your URLs so that content changes are reflected in the URL, not in the content of a static file.
Only After All That, Consider Redis
If you’ve exhausted edge caching and HTTP headers, then, and only then, should you reach for a distributed cache like Redis. Yes, Redis is fast—typical hit rates of 90–95% and 1–5 ms latency (AWS caching overview). But it’s also another moving part. If you must use Redis, use the cache-aside pattern: check the cache first, fetch from the database on a miss, then write back (AWS caching overview). And for eviction, don’t overthink it: Redis uses LRU by default, and the recommendation is allkeys-lru because a cache is not storage (AWS caching overview).
But here’s my beef: teams often use Redis to compensate for a lack of HTTP caching discipline. They cache a database query result that could have been cached at the edge as a static JSON file. That’s not clever; it’s wasteful. Redis has its place—for session data, rate limiting counters, or complex aggregations that can’t be precomputed—but it shouldn’t be your first cache.
What I’d Actually Do
Here’s my concrete recommendation, and it’s not sexy but it works. First, audit every endpoint and classify it: static (identical every time), semi-dynamic (changes rarely), or dynamic (changes per user). For static and semi-dynamic, set Cache-Control: public, max-age=60, stale-while-revalidate=600 and ensure an ETag. For dynamic, use a CDN edge that supports tiered caching and let Vary handle the personalization. Second, put a load balancer like nginx in front of your origin and enable its proxy cache for the semi-dynamic responses—nginx’s proxy_cache_path with proxy_cache_valid can cache 200 and 302 responses for 10 minutes (Nginx proxy module). Third, if you still have hot spots, add Redis for specific high-cost queries, but keep it as a last resort. I’d bet that most teams that follow this path will see their edge cache absorb 70% or more of origin requests (AWS caching overview), and their Redis usage will drop to near zero. That’s not a caching strategy; that’s a load-balancing strategy. And it’s the one I’d stake my weekend on.
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 (tiered cache) - https://developers.cloudflare.com/cache/how-to/tiered-cache/
- Nginx (proxy module) - https://nginx.org/en/docs/http/ngx_http_proxy_module.html
- 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!