# HTTP caching and personalised content: who may store a response, and for whom

> An HTTP cache decides three things before it reuses a response: whether it may store it at all, which future requests it matches, and whether it is still fresh. Shared and private caches, the cache key and Vary, freshness and validation, what no-cache, no-store and private actually say, and why a personalised page behind a shared cache is an architecture question, not a TTL setting.

Source: https://ronutz.com/en/learn/http-caching-and-personalised-content  
Updated: 2026-10-26  
Related tools: https://ronutz.com/en/tools/http-message-decoder, https://ronutz.com/en/tools/nginx-proxy-cache-decision

---

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](https://www.rfc-editor.org/rfc/rfc9111), 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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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. RFC 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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.2)). 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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3)); a private cache ignores `s-maxage` itself, which speaks to shared caches only ([section 5.2.2.10](https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10)). A heuristic is allowed only when none of them is present ([section 4.2.2](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.2.1)). A 200 is one of the status codes defined as heuristically cacheable ([RFC 9110, section 15.1](https://www.rfc-editor.org/rfc/rfc9110.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#section-3)). 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](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.3)). It is fresh while its lifetime is greater than that age ([section 4.2](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2)), and a cache that reuses it without validation sends an `Age` header carrying the current age ([section 4](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.8)). When it validates, a cache should send the stored `Last-Modified` as `If-Modified-Since` ([section 4.3.1](https://www.rfc-editor.org/rfc/rfc9111.html#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](https://www.rfc-editor.org/rfc/rfc9111.html#section-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](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.1)). A number of seconds too large to represent is taken as 2147483648 ([section 1.2.2](https://www.rfc-editor.org/rfc/rfc9111.html#section-1.2.2)).

*Interactive lab: Decide whether a cache may reuse a response (use it on the page: https://ronutz.com/en/learn/http-caching-and-personalised-content#lab-http-freshness)*

## 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](https://ronutz.com/en/tools/http-message-decoder) shows the fields a response carries, and [NGINX caching: what gets stored, what gets served](https://ronutz.com/en/learn/nginx-proxy-cache-what-gets-stored) 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)](https://www.rfc-editor.org/rfc/rfc9111)
- [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)](https://www.rfc-editor.org/rfc/rfc9110)
