A cache reuses a response it stored earlier to answer a new request, which saves a round trip to the origin and the work of building the answer again. HTTP's caching rules exist because that reuse is only correct under conditions, and the most expensive mistake in the area is not a stale page; it is one user's page served to another. The rules are in RFC 9111, and they turn on three questions: may this response be stored, which later requests does it answer, and is it still fresh.
Shared caches and private caches
The specification draws one distinction before any other. A shared cache stores responses for reuse by more than one user; it usually sits in an intermediary such as a reverse proxy or a content delivery network. A private cache is dedicated to a single user, typically the browser's own (RFC 9111, section 1). A response that is perfectly safe to keep in one person's browser can be dangerous in a shared cache, and several directives exist precisely to say which kind of cache may keep what.
Which requests a stored response answers
A cache may reuse a stored response only for a request with the same target URI, a method the stored response allows, and, if the response named any request header fields in Vary, the same values for those fields (RFC 9111, section 4). Vary therefore narrows reuse: a response stored with Vary: Accept-Language is reused only for later requests whose Accept-Language matches the request that produced it, and a response with Vary: * never matches at all (section 4.1). The URI plus whatever Vary names is, in effect, the cache key.
That is where personalised content goes wrong. If a page differs from user to user because of a cookie or a token, and the response neither forbids shared storage nor names the field that personalises it, a shared cache sees one URI and one stored answer, and serves it to the next person who asks. 9111 is explicit that a Set-Cookie header does not stop a response from being cached and is often served from caches to later requests; servers that want to control that must say so with Cache-Control (section 7.3). One exception is built in: a shared cache must not reuse a response to a request that carried an Authorization header unless the response explicitly allows shared storage with public, s-maxage or must-revalidate (section 3.5).
What the directives actually say
Three Cache-Control response directives are confused more than any others, because their names suggest more than they do:
no-store: a cache must not store any part of the request or the response (section 5.2.2.5). This is the one that means "do not keep this".no-cache: the response may be stored, but must not be used to satisfy a later request without first being validated with the origin and getting a successful answer (section 5.2.2.4). It means "check before reusing", not "do not cache".private: a shared cache must not store the response; a private cache may (section 5.2.2.7). This is the directive for a page meant for one user.
A personalised page can therefore be cached safely in two ways: privately, in the user's own browser, by marking it private; or in a shared cache with the personalising request field in Vary, so that each variant is stored under its own key. Lengthening or shortening the freshness lifetime changes neither of those outcomes: a page stored under a key that is too broad is wrong for one second as surely as for one day.
Freshness and validation
A stored response is fresh while it can be reused without asking the origin. Its lifetime comes from max-age, s-maxage for shared caches, or Expires, and when none is present a cache may assign a heuristic lifetime, which the specification suggests should be no more than a fraction of the time since Last-Modified, typically 10% (sections 4.2.2 and 5.2.2.10). A response that is no longer fresh is not necessarily discarded: the cache can validate it with a conditional request, and the origin either confirms that the stored response is still good or sends a new one (section 4.3). no-cache simply makes that validation mandatory on every reuse.
The arithmetic is short enough to do by hand. A cache takes the freshness lifetime from the first of these that applies: s-maxage if the cache is shared, then max-age, then Expires minus Date (section 4.2.1). A response with max-age makes every cache ignore Expires, and one with s-maxage makes a shared cache ignore it (section 5.3); a private cache ignores s-maxage itself, which speaks to shared caches only (section 5.2.2.10). A heuristic is allowed only when none of them is present (section 4.2.2), and the 10% is a typical setting the specification mentions, not a rule, so whether a cache uses it is the cache's own choice. An origin server with a clock never sends a Last-Modified later than its Date (RFC 9110, section 8.8.2.1). A 200 is one of the status codes defined as heuristically cacheable (RFC 9110, section 15.1), so a 200 response to a GET may be stored with no Cache-Control at all, and public adds nothing to it (RFC 9111, sections 3 and 5.2.2.9). The response's current age starts from the larger of its apparent age, the moment of receipt minus Date and never below zero, and its Age header plus the time the request took; the time it has spent in this cache is then added (section 4.2.3). It is fresh while its lifetime is greater than that age (section 4.2), and a cache that reuses it without validation sends an Age header carrying the current age (section 4).
Stale does not mean unusable. A cache must validate a stale response before reusing it, unless it is disconnected from the origin or explicitly allowed to serve it stale, and it never serves it stale against no-cache, must-revalidate or, in a shared cache, s-maxage or proxy-revalidate (section 4.2.4). With must-revalidate, a disconnected cache answers with an error, which should be 504 (Gateway Timeout), rather than with the stale response (section 5.2.2.2); proxy-revalidate says the same to shared caches only, and s-maxage carries that rule with it (sections 5.2.2.8 and 5.2.2.10). When it validates, a cache should send the stored Last-Modified as If-Modified-Since (section 4.3.1). Directive names are compared case-insensitively, and a cache ignores the directives it does not recognise (sections 5.2 and 5.2.3); when a directive appears twice, the specification lets the cache use the first or treat the response as stale, and it encourages treating invalid freshness information, such as a max-age that is not a whole number, as stale (section 4.2.1). A number of seconds too large to represent is taken as 2147483648 (section 1.2.2).
Interactive lab
Decide whether a cache may reuse a response
A 200 response to a GET sits in a cache. Set its headers, the kind of cache and how long ago the response arrived, and follow the cache's reasoning: may it store the response, how long it stays fresh and why, how old it is now, and whether it may answer without asking the origin. The Example shows s-maxage overruling max-age in a shared cache only; switch the cache to private, then try no-cache.
What happens
- Store May store it: nothing forbids it, and a 200 is heuristically cacheable, so it needs no directive to be stored.
- Freshness lifetime 300 s, from max-age.
- Current age 90 s: a starting age of 30 s (the larger of the apparent age, 2 s, and Age, 30 s), plus 60 s in this cache.
- Fresh or stale Fresh: the lifetime, 300 s, is greater than the age, 90 s, with 210 s to go.
- Origin reachable Answers from the cache without contacting the origin, sending Age: 90.
- Origin unreachable Answers from the cache: a fresh response needs no contact.
public changes nothing here: the response could be stored without it.
What the model leaves out: request directives such as max-stale, Vary and the cache key, requests carrying Authorization, status codes other than 200, the time the request took (the response delay is taken as zero), ETag, must-understand, and extensions such as stale-while-revalidate, which this cache does not recognise and so ignores; the heuristic, when chosen, is 10% of the time between Last-Modified and Date, rounded down.
Reading a cache problem
Most cache incidents reduce to the three questions in order. Was the response allowed to be stored where it was found, given private, no-store and Authorization? Was it chosen for a request it should not have answered, because the key, the URI plus Vary, left out what made the content different? And was it reused without validation when it was stale or marked no-cache? The HTTP message decoder shows the fields a response carries, and NGINX caching: what gets stored, what gets served shows how one widely deployed shared cache builds its key and applies the same directives.
Sources
- RFC 9111, HTTP Caching, June 2022, Internet Standard, neither obsoleted nor updated when checked on 2026-10-26 (its errata, none verified, do not touch the sections cited here): section 1 (shared and private caches), section 1.2.2 (delta-seconds and 2147483648), section 3 (when a response may be stored), section 3.5 (responses to requests with Authorization), section 4 (when a stored response may be reused; the Age a cache sends), section 4.1 (Vary and the cache key, Vary: * never matches), section 4.2 (fresh while the lifetime exceeds the age), section 4.2.1 (the order of s-maxage, max-age and Expires; repeated and invalid directives), section 4.2.2 (heuristic freshness, typically 10% of the time since Last-Modified), section 4.2.3 (calculating age), section 4.2.4 (serving stale responses), sections 4.3 and 4.3.1 (validation, If-Modified-Since), sections 5.2 and 5.2.3 (directive syntax, unrecognised directives ignored), sections 5.2.2.2, 5.2.2.4, 5.2.2.5, 5.2.2.7, 5.2.2.8, 5.2.2.9 and 5.2.2.10 (must-revalidate, no-cache, no-store, private, proxy-revalidate, public, s-maxage), section 5.3 (Expires ignored beside max-age or s-maxage) and section 7.3 (Set-Cookie does not inhibit caching) (read 2026-10-26)
- RFC 9110, HTTP Semantics, June 2022, Internet Standard, neither obsoleted nor updated when checked on 2026-10-26: section 8.8.2.1 (Last-Modified never later than Date) and section 15.1 (heuristically cacheable status codes, 200 among them) (read 2026-10-26)