Cache-Control Builder

Two caches read the same header and reach different conclusions. The browser obeys max-age; a shared cache in front of your origin prefers s-maxage if it is there and ignores anything marked private. Writing one number and assuming both behave the same is how a logged-in page ends up served to a stranger.

How long the browser may reuse the response without asking. Leave blank to omit.
Overrides max-age for shared caches only. Leave blank to omit.
How long a stale copy may be served while a fresh one is fetched in the background.
How long a stale copy may be served when the origin returns an error.
Cache-Control Builder — max-age, immutable and the RestBuildFigure

Fresh, stale, and the conditional request in between

A cached response has a freshness lifetime. Inside it, a cache serves the copy with no network involvement at all — the fastest request is the one that never leaves the machine. Outside it, the response is stale, which does not mean discarded. A stale response can be revalidated: the cache asks the origin whether its copy is still good, sending If-None-Match with the ETag or If-Modified-Since with the timestamp, and the origin answers 304 Not Modified with no body if nothing changed. That is a round trip, but a small one.

So there are three costs, not two: a hit costs nothing, a revalidation costs a round trip with an empty response, and a miss costs the full body. Most tuning is about moving requests up that list. max-age converts revalidations into hits, and a validator converts misses into revalidations.

max-age and s-maxage are read by different things

max-age applies to every cache, including the one in the visitor's browser. s-maxage applies only to shared caches, and where it is present it overrides max-age for them. Browsers do not read s-maxage at all.

That split is the most useful lever in the whole header, because the two caches have completely different properties. A shared cache in front of your origin is one you can usually purge, so a mistake in it is a minute of work. A browser cache is unreachable — nothing can recall a response once it is there, and if you set a year, you have committed to a year for anyone who took a copy. The shape that follows is a short max-age and a long s-maxage: the browser rechecks frequently and cheaply, the shared cache absorbs the traffic, and you keep the ability to change your mind.

The fingerprint pattern, and immutable

When a file's name contains a hash of its contents, a new version is a new URL, and the old URL will never serve different bytes. That makes the cache question trivial: cache it for as long as the header allows, because it can never be wrong. The conventional value is max-age=31536000, one year, with public and immutable.

immutable exists for one specific case. Without it, a browser revalidates on a manual reload even for a response that is still fresh, so a user pressing reload sends a conditional request for every asset on the page. immutable tells it not to bother, because the body at this URL will never change. It only makes sense with a long lifetime and a URL that genuinely cannot change, and on a stable-named file it is actively harmful: the browser will keep the old copy for the entire year and there is no way to reach it.

no-cache does not mean do not cache

The two most misread directives sit next to each other. no-cache means the response may be stored, but must be revalidated with the origin before every reuse. It is a caching strategy — a good one for HTML, where you want the 304 to be cheap but you never want a stale page. no-store means the response must not be written to a cache at all, in memory or on disk, and every request is a fresh one.

If the requirement is that a response never lands on disk, only no-store does that. private keeps it out of shared caches and permits the browser to store it. no-cache permits storage everywhere and merely forces a check. Reaching for the wrong one produces a configuration that reads as cautious and behaves as permissive.

Vary, and the cache poisoning that is not an attack

A cache keys on the URL. If the body varies by something else — the accepted encoding, the language, the signed-in user — the cache needs to be told, and Vary is how. Vary: Accept-Encoding is nearly always correct where compression is negotiated. Vary: Cookie is what stops a personalised page being handed to the next person, though marking it private is a better answer, because varying on cookie makes the hit rate approximately zero and still stores something you would rather nobody stored.

Vary: User-Agent is the one to avoid. It splits the cache by every browser build in existence, so the hit rate collapses and the origin absorbs traffic it was supposed to be shielded from. The intent behind it is nearly always achievable another way. And a missing Vary on a response that genuinely does vary is the everyday version of serving the wrong thing to the wrong person: not an attack, just a header nobody wrote. The record layer underneath all of this — where a name points and how long that answer is held — is the DNS record builder, and the response codes carrying these headers are in the status code reference.

Questions people ask

What is the difference between no-cache and no-store?

no-cache permits storage and requires revalidation before every reuse — the copy exists, it just cannot be served without checking. no-store forbids storage entirely, so nothing is written anywhere and every request goes to the origin in full. If the requirement is that a response never reaches disk, no-store is the only directive that says so. no-cache is a performance strategy; no-store is a handling instruction.

Should I use ETag or Last-Modified?

ETag, where you have a choice. Last-Modified has one-second resolution, so two changes in the same second are indistinguishable, and it relies on a file timestamp that many deployment processes reset for every file at once — which invalidates a whole cache for no reason. An ETag is whatever value you decide identifies the content, usually a hash. Where both are present, ETag takes precedence. Sending both is harmless and gives older intermediaries something to work with.

Why would I set a short max-age and a long s-maxage?

Because the two caches differ in one crucial way: you can purge a shared cache you control, and you cannot purge a browser cache. A long browser lifetime is a commitment you cannot take back — set a year, and a visitor holding a bad copy keeps it for a year. A long shared lifetime absorbs the same traffic and can be cleared in a minute. Short max-age, long s-maxage gives you most of the benefit and keeps the ability to fix a mistake.

Does immutable do anything on a page that changes?

Nothing good. It tells the browser not to revalidate during the freshness lifetime even when the user presses reload, which is exactly what you do not want on something that changes at the same URL. It is built for content-addressed files, where the URL changes whenever the bytes do, so the promise is trivially true. On anything else you have committed to serving a stale copy for the full lifetime with no way to intervene.

Can this tool check the headers my site is sending?

No. It builds a header from the values you enter and checks those values against each other and against the kind of response you said this is — no request is made and nothing is fetched, because everything runs inside the page. To see what a real response carried, capture the headers in a browser network panel and paste them into the header explainer, which describes each one it recognises.

Related