Skip to main content

There is an old joke that there are only two hard problems in computer science: cache invalidation, naming things, and off-by-one errors. It has aged well, because most teams still have not solved the first one. They have quietly stopped trying.

Walk into a typical mid-sized codebase and you will find the same artefact: a TTL somewhere between five minutes and an hour, chosen once during a performance incident and never revisited. Content changes, the cache does not, a client complains, someone lowers the TTL. Origin load creeps up, the hosting bill follows, someone raises it again. That is not a caching strategy. That is TTL roulette, and in 2026 there is no longer much excuse for it.

TL;DR

  • Time-based expiry is a guess. Event-driven invalidation via cache tags (Cloudflare) or surrogate keys (Fastly) lets you cache aggressively and purge precisely.
  • Cloudflare opened Cache-Tag to all plans in April 2025 and made stale-while-revalidate fully asynchronous on 26 February 2026, killing the first-request latency penalty.
  • Tag limits are design constraints: 16 KB Cache-Tag header (roughly 1,000 tags), 1,024 characters per tag, printable ASCII, no spaces.
  • The browser cache is the one layer you cannot purge. Content-hashed filenames plus immutable are the only safe pattern.
  • Cache stampede, not staleness, is what takes origins down. Request coalescing belongs in the application tier from day one.
  • AI crawler and agent traffic is flattening the repeat-request patterns caches depend on, and a careless Vary header destroys hit ratios.

Event-driven invalidation is table stakes now

TTL roulette has a price tag. A five-minute TTL on a page that changes twice a month means roughly 8,600 unnecessary origin requests per month for that one URL, and users still see content up to five minutes stale: high origin cost and incorrect content, both at once. The fix is not a longer TTL on its own. It is a longer TTL plus a purge that fires the moment content changes, so freshness becomes a function of your publishing workflow rather than a stopwatch.

Tag-based purging is the mechanism, and it has quietly become available to everyone. Fastly has shipped surrogate keys since around 2014 and remains the reference implementation, with global tag purges propagating in roughly 150 milliseconds. Cloudflare shipped Cache-Tag in 2016 as Enterprise-only and opened it to all plans in April 2025, which matters more than it sounds: the most useful caching primitive stopped being a six-figure-contract privilege.

Your origin attaches tags describing what each response depends on:

Cache-Tag: product-4417, category-terrariums, pricing-eur, nav-v3

When product 4417 changes price, you POST that one tag to the purge API. Every cached object worldwide carrying it goes stale at once: the product page, the category listing, the search results, the JSON response feeding your mobile app. You did not have to enumerate the URLs, or even know they existed. That is the real win, because the URLs you forget about are exactly the ones that serve a stale price to a customer.

Design around the limits first. Cloudflare caps the aggregate Cache-Tag header at 16 KB after the field name, roughly 1,000 unique tags per response. Individual tags are capped at 1,024 characters for API purges, must be printable ASCII, cannot contain spaces, and are case-insensitive. Teams generating tags from product names find those last rules the hard way. Normalise at generation, not at purge.

Stale-while-revalidate finally behaves the way it reads

RFC 5861 defined stale-while-revalidate years ago, and it has been the most underused directive in HTTP ever since, largely because of implementation. On several CDNs the first request after expiry still blocked while the edge went back to origin. The directive promised instant responses and delivered them to everyone except the person who triggered the refresh.

Cloudflare changed this on 26 February 2026. SWR is now fully asynchronous: the first request after expiry triggers background revalidation and immediately receives the stale copy with an UPDATING cache status, switching to HIT once the refresh completes. It is live across Free, Pro and Business zones, with Enterprise rolling out through the quarter. Fastly and CloudFront support edge-level SWR too.

That is more consequential than a latency win. Previously the first request after expiry was directly exposed to origin timeouts. Now a brief origin outage during a revalidation window is invisible to users. Pair it with stale-if-error and your CDN becomes an availability layer, not just a bandwidth layer:

Cache-Control: public, s-maxage=2592000, stale-while-revalidate=86400, stale-if-error=604800

Thirty days of edge freshness, a day of grace while refreshing, and a week of serving from cache if the origin falls over entirely. Combined with tag purging, that thirty-day TTL costs you nothing in staleness.

The layer you cannot purge

Every invalidation discussion eventually collides with the browser cache, which has no purge API and never will. Once you have sent max-age=31536000 to a user’s browser, that asset is out of your hands for a year.

The only safe pattern is content-hashed URLs. Build tooling that emits app.9f3c2a1b.js lets you set max-age=31536000, immutable with confidence, because a changed file is a changed URL. That directive stops browsers revalidating on reload, and is only ever safe on hashed filenames. Applying it to /logo.png is how teams end up asking clients to hard-refresh, which is an admission that the architecture failed.

The corollary: HTML documents should almost never carry a long browser TTL. Cache the document at the edge with s-maxage, which browsers ignore by design, and keep max-age short or zero. The edge is purgeable. The browser is not.

Stampede is the failure mode that actually hurts

Staleness makes clients unhappy. Cache stampede makes your origin fall over. A popular key expires, several hundred concurrent requests miss at once, and every one hits the database with the same expensive query. The cache was the only thing between your traffic and an unindexed join, and it stepped aside for two seconds.

Three mitigations, in ascending order of elegance:

  • Distributed locking. A Redis SETNX with an expiry, so one process regenerates while others wait or serve stale. Simple, but you must handle the lock holder crashing.
  • Request coalescing. The singleflight pattern collapses N identical in-flight requests into one origin call. Cheaper than locking because it is in-process, and the right default for read-heavy services.
  • Probabilistic early expiry. Refresh keys slightly before their nominal TTL, with probability rising as expiry approaches, so regeneration spreads across time instead of arriving as a thundering herd.

All three are stale-while-revalidate one layer down: serve instantly, refresh lazily. That principle now appears in RFC 5861, in Redis patterns, and in framework APIs such as use cache and cacheLife in Next.js 16. When the same pattern emerges independently at four layers of the stack, adopt it deliberately rather than by accident.

Personalisation quietly destroys your hit ratio

The fastest way to make a well-designed cache useless is a careless Vary header. Vary: Cookie on a site with session cookies means every user gets their own cache entry, which in practice means nobody gets one. Hit ratios collapse and nobody notices, because the site still works. It is just slower and considerably more expensive.

The fix is architectural, not configurational. Split personalised fragments out of otherwise cacheable documents: serve the shell from cache and hydrate personalised parts client-side, or use edge compute to assemble a cached shell with a small uncached fragment. Where you genuinely must vary, vary on a normalised low-cardinality value such as device class or country code, never a raw header you do not control.

AI traffic is changing the maths

Bot and crawler traffic now accounts for the majority of requests to many sites, and AI crawlers behave nothing like traditional search bots: they fetch deep into the long tail, rarely revisit, and show little interest in your popular pages. Caches earn their keep on repeat requests for the same objects, so that shift directly erodes hit ratios.

Agent traffic compounds it. An AI agent hitting your API on a user’s behalf requests specific, authenticated, low-repetition data, precisely the shape that caches worst. If you are building agent-facing endpoints, cacheability needs to be a design input: stable resource identifiers, ETags so revalidation stays cheap even when caching is not viable, and clean separation between the personalised and shared parts of every response.

Where to start

If your caching is currently one TTL and some optimism:

  1. Measure hit ratio per content type, not in aggregate. An 85% overall ratio can hide an 8% ratio on the pages that cost most to render.
  2. Audit your Vary headers. Most teams have at least one they did not intend to ship.
  3. Add tags before you extend TTLs. Invalidation capability first, aggressive caching second. The other way round is how you ship stale prices.
  4. Wire purges into your publishing workflow, not a deployment script. Content changes far more often than code.
  5. Add stale-while-revalidate and stale-if-error to everything cacheable. There is essentially no downside now revalidation is asynchronous.
  6. Fix stampede in the application tier, because no CDN protects you from a cache miss on an expensive query.

Caching is one of the few areas of architecture where getting it right makes a system faster and cheaper at once. Most teams leave that on the table because invalidation feels risky, so they cache timidly and pay twice: in origin cost, and in the performance they never gained. With tag-based purging now on every plan tier and asynchronous revalidation the default, timid caching is a choice rather than a constraint.

Need help with this?

REPTILEHAUS designs and builds caching and delivery architecture for SaaS platforms, ecommerce sites and content-heavy applications, alongside our wider development, DevOps, AI and Web3 work. If your origin bill is climbing, your hit ratio is a mystery, or you could not confidently purge a single product across every surface it appears on, get in touch.

📷 Photo by Albert Stoynov on Unsplash