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/HEADcache 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-EncodingThat's the safe one (gzip vs br vs none). Avoid:
| Vary header | Why bad |
|---|---|
Vary: * | Effectively uncacheable |
Vary: Cookie | Unbounded fragmentation |
Vary: User-Agent | Hundreds 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-aliceNow a post update purges everything tagged post-42. A new feature on the blog purges blog. Banning an author purges author-alice.
| Surrogate-key system | Header | Notes |
|---|---|---|
| Fastly | Surrogate-Key | First-class; the canonical implementation |
| Cloudflare | Cache-Tag (Enterprise) | Same idea |
| Varnish | xkey module | Self-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=86400What 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) ──► OriginA 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:
| Optimization | Effect |
|---|---|
| Resize | Send a 400px image to mobile, not 4000px |
Format conversion (auto) | Serve webp / avif to supporting browsers; jpeg to others |
| Quality | 75-85 is indistinguishable from 100 in most cases |
| Strip metadata | EXIF data is bytes nobody needs |
| Lazy load | Combine 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: AcceptMost image CDNs handle the Vary: Accept normalization for you ("Accept" → "supports avif | supports webp | jpeg only").
Cookie-Based Bypass
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:
- Strip cookies at the CDN edge for cacheable paths (Cloudflare: Cache Rules → ignore cookies for
/blog/*). - Mark cookies
HttpOnly+Secure+ scoped path so they only attach to paths that need them. - Use different domains for public-static (
cdn.example.com) and authenticated (app.example.com). - 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=600This 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.