Everyone's obsessed with the perfect eviction policy, the ideal TTL, the magical CDN setting that makes your cache hit 95%. We've all been there: tweaking Redis maxmemory-policy, arguing over LRU versus LFU, and setting Cache-Control max-age to some arbitrary number that we hope balances freshness and performance. But we're missing the point. The real breakthrough isn't about when to evict a cached response; it's about what to serve while that response is being revalidated. The future of caching is stale-while-revalidate, and it's time we start using it.
The Cache Control Illusion
We treat Cache-Control max-age like it's a magic dial: set it to 3600 for an hour of freshness, or 604800 for a week. But the moment we set a finite TTL, we're making a compromise. Too short, and we're hammering the origin with revalidation requests. Too long, and we risk serving stale content for an eternity. The fact base is clear: max-age=N keeps a response fresh for N seconds after it's generated, but that's it—after that, the clock starts ticking on staleness (MDN, Cache-Control). We've accepted this binary: fresh or stale, serve or revalidate. But what if we could have both?
Embrace Staleness with stale-while-revalidate
RFC 5861 introduced two extensions to Cache-Control that change the game: stale-while-revalidate and stale-if-error. The first allows a cache to serve a stale response for a specified period while it revalidates the resource in the background. The second lets a cache serve stale content if the origin returns an error, like a 500 or a DNS failure (RFC 5861). This is not about being lazy or accepting outdated data; it's about prioritizing availability and speed over the illusion of perfect freshness.
Consider a typical product page on an e-commerce site. The price might change, but not every second. With a max-age of 60 seconds, you might serve stale content for up to a minute, then revalidate. With stale-while-revalidate=30, you can serve a response that's up to 30 seconds stale while fetching the new version in the background. The user sees the page instantly, the origin gets a request, and the cache is updated without the user ever waiting. This is a win-win.
But What About the Counter-Argument?
You'll hear the objection: "Stale content is bad. Our users need the freshest data, period." But look at the numbers. AWS reports that Redis caches typically hit 90–95% with 1–5 ms latency, and adding a cache can reduce database CPU by 70–90% (AWS, caching overview). CDNs commonly achieve hit ratios of 80–95% and can absorb 70% or more of origin requests (AWS, caching overview). If you're serving content that's stale by 5 seconds, but you're offloading 90% of your traffic, that's a tradeoff most businesses would take. And stale-while-revalidate doesn't make content permanently stale; it's a temporary state that lasts only as long as the revalidation takes.
But what about dynamic content that changes on every request? That's a valid concern. For truly dynamic data—like a user's shopping cart or a stock ticker—you shouldn't be caching it at all, or you should use something like no-store. But for the vast majority of web content—product pages, blog posts, images, even API responses that don't change every second—stale-while-revalidate is the perfect fit. The key is to apply it selectively, not as a blanket policy.
How to Implement It
Start by auditing your Cache-Control headers. If you're currently setting max-age=3600, consider changing it to max-age=60, stale-while-revalidate=3600. This way, the response is fresh for 60 seconds, but if a request comes in after that, the cache can serve a stale version for up to an hour while it revalidates. The user never waits, and the origin only gets hit once per hour per resource, instead of once per minute.
For CDNs like Cloudflare, you can also use cache purging to invalidate a specific file across all edge locations instantly (Cloudflare, purge by single-file). This is useful when you need to force an update, but it's the exception, not the rule. In your application layer, if you're using Redis, consider using the SETEX command to set a value with a TTL, and then implement a background revalidation pattern that refreshes the key before it expires (Redis, SETEX).
And for load balancers, if you're worried about cache friendliness, consider using consistent hashing to ensure the same client always hits the same server, which can improve cache hit ratios (Nginx, upstream module). But even that is secondary to getting your cache headers right.
The Takeaway
Stop obsessing over eviction policies and TTLs as if they're the only levers you have. The real power lies in serving stale content while revalidating in the background. It's not about sacrificing freshness; it's about optimizing for speed and availability. By embracing stale-while-revalidate, you can dramatically cut origin load, improve perceived performance, and keep your users happy. And isn't that the whole point of caching?
Sources
- MDN (Cache-Control) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
- RFC 5861 (stale-while-revalidate) - https://www.rfc-editor.org/rfc/rfc5861.txt
- AWS (caching overview) - https://aws.amazon.com/caching/
- Cloudflare (purge by single-file) - https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-single-file/
- Redis (SETEX) - https://redis.io/docs/latest/commands/setex/
- Nginx (upstream module) - https://nginx.org/en/docs/http/ngx_http_upstream_module.html
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!