Guide

HTTP caching is set entirely by headers, and both browsers and CDNs follow them. Getting the headers right is one of the cheapest performance wins available. The same headers, set carelessly, can also leak one user's data to another.

Concept

Before you start: a cache keeps a copy of a response so the next request can skip the trip to the origin. A validator is a version fingerprint, such as an ETag, that lets a cache check whether its copy is still current.

Cache-Control

Cache-Control is the main control. Its directives are:

  • max-age=31536000, immutable: cache for a year and never revalidate. Use this for fingerprinted assets such as app.3f9a1c.js, whose URL changes when the content does.
  • no-cache: the response may be stored, but the cache must revalidate with the server before each use. Good for HTML.
  • no-store: never store it. Use this for sensitive responses such as bank statements.
  • private or public: private means only the user's browser may cache it, not shared caches such as CDNs. Anything personalized must be private.
  • s-maxage=60: a separate TTL for shared caches. The CDN can keep a copy for 60 seconds while browsers revalidate.
  • stale-while-revalidate=30: serve the stale copy for up to 30 seconds while refetching in the background.

Walking through it: a conditional request

Validators make revalidation cheap. The server sends an ETag (a version fingerprint) or a Last-Modified timestamp. When the cached copy expires, the client asks with a conditional request:

HTTP
GET /v1/catalog HTTP/1.1
If-None-Match: "a1b2c3"

HTTP/1.1 304 Not Modified
ETag: "a1b2c3"
Cache-Control: max-age=60

A 304 has no body. If the catalog is 400 KB, revalidation costs one round trip and a few hundred bytes, not 400 KB. The cache keeps its copy and refreshes its timer.

Vary

Vary tells caches which request headers change the response. Vary: Accept-Encoding means gzip and Brotli versions are cached separately.

Be careful with Vary: Cookie or Vary: User-Agent on a CDN. Each distinct value creates a separate cache entry. With cookies, that is almost one entry per user, which destroys the hit rate.

Browser cache vs CDN cache

The browser cache serves one user and saves the whole network trip. A CDN serves everyone near a point of presence and saves the trip to your origin.

A common pattern pairs the two:

  • HTML: Cache-Control: public, max-age=0, s-maxage=60. The CDN caches for a minute, and browsers always check.
  • Static assets: fingerprinted URLs with a one-year immutable lifetime.
index.html        public, max-age=0, s-maxage=60     → CDN keeps it 60 s
app.3f9a1c.js    public, max-age=31536000, immutable → cached for a year

Common mistakes

  • Marking personalized pages public. A shared cache can serve one user's page to another.
  • Using Vary: Cookie on a CDN. The hit rate collapses to almost nothing.
  • Caching HTML for a year. Users then keep an old page that points at removed assets.
  • Using no-store everywhere. It disables caching entirely. Reserve it for sensitive responses.

Check yourself

  • Why can a 304 response save bandwidth for a 400 KB catalog? The client sends its ETag, the server confirms it still matches, and no body is sent.
  • Why is a fingerprinted asset safe to cache for a year? Its URL changes whenever its content changes, so an old copy is never served for new content.

Key takeaways

  • Cache-Control says who may store a response and for how long. private and s-maxage separate browsers from CDNs.
  • ETag plus If-None-Match turns a full download into a cheap 304 check.
  • Vary must name only headers that really change the response. Cookies in Vary destroy cache hit rates.
  • Fingerprinted assets with immutable, and revalidated HTML, is the standard pattern.

Further learning

Next, see how cookies, CORS and streaming shape what browsers send and receive, in cookies, CORS and streaming.