Frontend Labs
⋯

59. Caching: the highest-leverage HTTP feature

59.1 The three caches in the path

browser cache  ->  (shared) CDN / proxy cache  ->  origin
   private            shared                       source of truth

Private caches hold one user's data; shared caches serve many users. The distinction drives every directive below.

59.2 Cache-Control directives that matter

Response directives:

Directive Meaning
max-age=N Fresh for N seconds (relative to Date)
s-maxage=N Overrides max-age for shared caches only
public May be stored by shared caches even if normally not (e.g. with Authorization)
private Browser only — CDNs must not store it
no-cache May store, but must revalidate before every reuse. (Not "don't cache".)
no-store Never write to disk/memory. The real "don't cache".
must-revalidate Once stale, do not serve stale — revalidate or fail
immutable Content will never change; don't revalidate even on reload
stale-while-revalidate=N Serve stale for up to N s while refreshing in the background
stale-if-error=N Serve stale for up to N s if the origin errors

The single most common bug: no-cache does not mean "don't cache". If you mean that, say no-store.

59.3 Validators and conditional requests

Origin response:            Later request:                 Origin reply:
  ETag: "abc123"              If-None-Match: "abc123"   ->   304 Not Modified (no body)
  Last-Modified: <date>       If-Modified-Since: <date> ->   304 Not Modified
  • ETag is an opaque version token. Strong ("abc") means byte-identical; weak (W/"abc") means semantically equivalent (fine for caching, not for range requests).
  • A 304 carries headers but no body — that's the whole saving.
  • If-Match / If-Unmodified-Since are the write direction: optimistic concurrency ("only update if it's still the version I read"), answering 412 Precondition Failed on conflict.

59.4 Vary — the correctness trap

Vary tells caches which request headers change the response.

Vary: Accept-Encoding        cache gzip and identity separately   (almost always needed)
Vary: Accept-Language        one entry per language
Vary: Cookie                 -> effectively uncacheable in a shared cache
Vary: User-Agent             -> cache explosion; avoid

Get it wrong in either direction and you either serve the wrong variant to someone, or destroy your hit rate. Vary: * means "never reuse".

59.5 Cache keys and CDN behaviour

A CDN's cache key defaults to roughly (method, scheme, host, path, query) plus whatever Vary demands. Operationally you often want to normalise it:

  • Strip tracking params (utm_*, fbclid) from the key so they don't shard your cache.
  • Ignore unknown query params for static assets.
  • Add a header to the key deliberately (e.g. device class) rather than Vary: User-Agent.

Cache-busting pattern: serve immutable assets with a content hash in the filename and Cache-Control: public, max-age=31536000, immutable; serve the HTML that references them with no-cache. The HTML revalidates cheaply; assets never do.

59.6 Invalidation

Method Notes
TTL expiry Simplest; you wait
Purge / invalidate API CDN-specific; may take seconds to propagate globally
Surrogate keys / cache tags Tag responses (Surrogate-Key: product-42), purge by tag. The best pattern for dynamic sites.
Versioned URLs No invalidation needed at all — the URL changes

Surrogate-Control is a CDN-only sibling of Cache-Control (stripped before reaching the browser), letting you cache aggressively at the edge and not at all in the browser.

59.7 Real-world example

An e-commerce site had a 4% CDN hit rate. Cause: the origin sent Set-Cookie on every response (a session cookie, even for anonymous static pages), and the CDN — correctly — refuses to share a response carrying Set-Cookie. Fix: only set the cookie on pages that need it, mark static assets public, max-age=31536000, immutable with hashed filenames, and add Surrogate-Key tags for product pages. Hit rate went to 94%; origin load fell ~20x.

Try it yourself

curl -sI https://example.com | grep -iE 'cache-control|etag|last-modified|age|vary'

# conditional request -- watch the 304
ET=$(curl -sI https://example.com | awk -F': ' '/[Ee]tag/{print $2}' | tr -d '\r')
curl -sI -H "If-None-Match: $ET" https://example.com | head -1

# see whether a CDN hit or missed (header name varies by vendor)
curl -sI https://www.cloudflare.com | grep -iE 'cf-cache-status|age|x-cache'

Generated from real-life-example.md · 8 Oct 2026 16:36