Field Notes
All posts

A field guide to Cache-Control

max-age, s-maxage, stale-while-revalidate and the directives that quietly decide how fast your site feels.

One header, many audiences

The Cache-Control header is read by at least three different parties: the visitor's browser, any shared cache in between, and your own edge network. Each one interprets the directives slightly differently, and most caching bugs come from writing a header for one audience while forgetting the others.

The directives that matter

max-age

max-age=3600 tells every cache that the response is fresh for an hour. During that hour a browser will not even ask the server; it will reuse what it has. That is wonderful for a logo and terrible for a price.

s-maxage

s-maxage applies only to shared caches. A common pattern is max-age=0, s-maxage=60: browsers always revalidate, but the edge can serve the same copy to everyone for a minute. Visitors get fast responses, and nobody holds a stale copy on their own device for long.

stale-while-revalidate

This directive lets a cache serve a stale response while it fetches a fresh one in the background. The visitor never waits on the refresh. For content that changes occasionally, it is the single most effective directive available.

no-store versus no-cache

The names are famously misleading. no-cache means the response may be stored but must be revalidated before every use. no-store means it must not be stored at all. Use no-store for anything personal, such as an account page or a checkout.

Common mistakes

  • Caching personalized HTML at the edge because a header was copied from a static asset rule.
  • Forgetting Vary, so a response compressed for one client is served to another that cannot read it.
  • Long max-age on unversioned assets, which turns a deploy into a week of mixed old and new files.
  • Setting cookies on cacheable responses, which many caches treat as a reason to skip caching entirely.

Fingerprint your assets

The cleanest strategy for static files is to put a content hash in the filename and cache it for a year with immutable. When the file changes, its name changes, and the HTML that references it points somewhere new. There is never a reason to invalidate anything.

HTML is different

HTML is the one resource that cannot be fingerprinted, because its URL is the address people type. Treat it with short shared-cache lifetimes, or render it fresh on every request and let everything it references be cached aggressively. The second approach keeps content correct to the second at the cost of some server work, and with a fast origin that cost is often smaller than people assume.

Test what you ship

Read the headers your production responses actually carry, not the ones in your configuration file. A proxy, a framework default, or a plugin can rewrite them on the way out, and the only reliable source of truth is the response itself.