Steven's Knowledge

Caching Strategies

Cache-Control in depth, surrogate keys, stale-while-revalidate, Vary, image optimization, dynamic content

Caching Strategies

Getting Started covered the basics. This page is about the strategies that separate "we put it on a CDN" from "95% of bytes never reach origin."

Cache Key Composition

A CDN keys cached responses by some combination of:

  • URL (always)
  • Host (per domain)
  • Method (only GET/HEAD cache by default)
  • Vary header values
  • Optionally cookies, query strings, regions, devices

Every dimension you add multiplies cache entries. Done carelessly, you fragment the cache and the hit rate collapses.

Query Strings

Default: most CDNs include query strings in the key. So ?utm_source=twitter and ?utm_source=facebook cache separately even though they're the same page.

Fix: strip or normalize query strings at the CDN. Cloudflare Cache Rules: "Ignore query string." Fastly VCL: set req.url = querystring.remove(req.url);.

Whitelisted parameters that actually affect the response (?page=2, ?lang=ja) stay; tracking parameters get stripped.

Vary Header

Vary tells caches "this response varies on the listed request header — cache separately per value":

Vary: Accept-Encoding

That's the safe one (gzip vs br vs none). Avoid:

Vary headerWhy bad
Vary: *Effectively uncacheable
Vary: CookieUnbounded fragmentation
Vary: User-AgentHundreds of UA strings; entropy ruins hit rate

Better than Vary: User-Agent: normalize to a small bucket on the origin (X-Device: mobile|tablet|desktop), Vary: X-Device.

Surrogate Keys / Cache Tags

The most powerful CDN feature most people don't use. Origin attaches tags to responses; later you purge by tag instead of by URL.

# Response from origin
Cache-Tag: post-42, blog, author-alice

Now a post update purges everything tagged post-42. A new feature on the blog purges blog. Banning an author purges author-alice.

Surrogate-key systemHeaderNotes
FastlySurrogate-KeyFirst-class; the canonical implementation
CloudflareCache-Tag (Enterprise)Same idea
Varnishxkey moduleSelf-hosted

Without surrogate keys you end up over-purging (purge_everything) or maintaining brittle URL lists.

stale-while-revalidate / stale-if-error

These two Cache-Control directives are gold:

Cache-Control: public, max-age=60, stale-while-revalidate=300, stale-if-error=86400

What this means at the edge:

  • 0-60s after fetch: serve from cache directly.
  • 60-360s after fetch: serve stale immediately, refresh in the background. Users never wait.
  • Origin returns 5xx: serve stale for up to 24h. Outage at origin doesn't break users.

stale-while-revalidate is the single best knob for "fast and always-current." Use it aggressively on public content.

Cache Hierarchies and Shielding

A CDN has many POPs, so a cold cache means N POPs all hitting origin for the same content. Cloudflare's Tiered Cache, Fastly's shielding, CloudFront's regional edge caches — they introduce a middle tier:

User POPs (200+) ──► Shield (1-2 POPs) ──► Origin

A cache miss in Tokyo asks the shield (in, say, Frankfurt). The shield asks origin once and fans out to other POPs that miss. Origin sees a small fraction of the traffic it otherwise would.

Worth turning on for any origin that's seeing meaningful CDN miss traffic.

Per-User and Personalized Content

CDNs cache shared content. Per-user content needs a different strategy:

1. Serve the shell, fetch per-user data with JS

<!-- Cached HTML for everyone -->
<div id="user-nav" data-loading="true"></div>

<script>
  fetch('/api/me').then(r => r.json()).then(renderNav);
</script>

The HTML is shared (caches well); the personalized bits load client-side from an uncached endpoint.

2. Edge personalization

Run logic at the edge (see Edge Functions) — read a cookie, decide the variant, fetch from cache with the variant in the key. Limited fan-out (a handful of buckets), much better hit rate than per-user.

3. Edge KV / KV-on-the-edge

Cloudflare Workers KV, Fastly KV Store: read user data at the edge without going to origin. Combine with edge personalization for cookie → user → response.

Image Optimization in Depth

The single biggest CDN win for most sites:

OptimizationEffect
ResizeSend a 400px image to mobile, not 4000px
Format conversion (auto)Serve webp / avif to supporting browsers; jpeg to others
Quality75-85 is indistinguishable from 100 in most cases
Strip metadataEXIF data is bytes nobody needs
Lazy loadCombine with loading="lazy" on <img>

A <img srcset> with multiple widths is the right HTML; let the CDN materialize each variant on demand.

Vary on Accept for Format Negotiation

To serve avif to browsers that support it and jpeg to others without duplicating URLs:

GET /photo.jpg
Accept: image/avif,image/webp,image/*

Cache key: /photo.jpg + Vary: Accept

Most image CDNs handle the Vary: Accept normalization for you ("Accept" → "supports avif | supports webp | jpeg only").

The default rule almost everywhere: request has cookies → bypass cache. Reasonable for logged-in users; tragic if your CMS sets a session cookie on every visitor.

Counter-measures:

  1. Strip cookies at the CDN edge for cacheable paths (Cloudflare: Cache Rules → ignore cookies for /blog/*).
  2. Mark cookies HttpOnly + Secure + scoped path so they only attach to paths that need them.
  3. Use different domains for public-static (cdn.example.com) and authenticated (app.example.com).
  4. For varied logged-in vs anonymous: vary on a single derived header (X-Authenticated: yes|no) instead of raw cookies.

Cache Negative Responses

404 Not Found is a response too — cache it briefly:

HTTP/2 404
Cache-Control: public, max-age=300, s-maxage=600

This is critical defense against scrape patterns that probe nonexistent URLs.

Push-Style: ESI and Edge-Side Includes

For pages that are mostly cacheable with small dynamic fragments, ESI lets you assemble at the edge:

<!-- Cached HTML shell -->
<header>...</header>
<main>...</main>
<esi:include src="/fragment/user-nav" />
<footer>...</footer>

The CDN fetches /fragment/user-nav separately (with its own cache rules), substitutes it in, returns assembled HTML. The shell stays cacheable; the personalized part is small.

Fastly supports ESI natively. Cloudflare Workers can simulate it. Most modern setups prefer client-side hydration instead, but ESI is the right tool for some use cases.

What's Next

You can craft cache rules and headers that yield 90%+ hit rates. Best Practices covers running CDNs in production — multi-CDN, security, observability, cost.

On this page