skip to content

Cache-Control appears on HTTP requests as well as responses. What do the request directives `no-cache`, `max-age=0`, `max-stale`, `min-fresh` and `only-if-cached` ask a cache to do, and how do they interact with what the origin declared?

level: middleimportance: nice to knowfreq 26%

answer

  1. response = origin policy, request = client tolerance
  2. request directives tighten; only max-stale loosens
  3. only-if-cached → cache hit or 504, never origin
  4. min-fresh = must survive N more seconds
  5. reload sends max-age=0; hard reload sends no-cache

basics

~20 s

On a request they express the client's tolerance. no-cache demands revalidation before any stored response is reused; max-age=0 says accept nothing older than zero seconds, so effectively the same; max-stale=N accepts responses stale by up to N seconds; min-fresh=N demands at least N seconds of remaining freshness; only-if-cached says answer from cache or return 504, never go to the origin.

solid answer

~60 s

Response directives are the origin's policy; request directives are the client's tolerance. A cache must satisfy both, and in general the request can only make the cache *stricter about reuse* — it cannot authorise reusing something the origin forbade. - **`no-cache`** — do not reuse a stored response without revalidating with the origin. This is what a browser sends on a normal reload. - **`max-age=0`** — no stored response older than zero seconds is acceptable, which forces revalidation in practice. - **`max-stale=N`** — the client will accept a response stale by up to N seconds (or any staleness if N is omitted). This is the one directive that *relaxes* reuse, and a cache must not honour it if the stored response carries `must-revalidate`. - **`min-fresh=N`** — only give me a response that stays fresh for at least N more seconds; useful when the client is about to go offline. - **`only-if-cached`** — answer from the cache or return `504 Gateway Timeout`; never contact the origin. Used for offline-first behaviour and cache probes. A request `no-cache` cannot make a `no-store` response cacheable; origin restrictions still bind.

code

bash · 3 lines
bash
curl -i -H 'Cache-Control: no-cache' https://example.org/report
curl -i -H 'Cache-Control: only-if-cached' https://example.org/report
curl -i -H 'Cache-Control: max-stale=300' https://example.org/report

go deeper

for a junior

Know that Cache-Control exists on requests too and that a reload sends one to force a fresh check.

for a middle

Be able to define each directive and state that only max-stale relaxes reuse while the rest tighten it.

for a senior

Use them as debugging tools — no-cache to test the origin, only-if-cached to probe storage — and explain browser reload semantics from them.

for a principal

Be clear on the boundary: HTTP request directives govern HTTP caches only, and any application-level cache bypass is a deliberate feature you must design and secure.

## Two sides of one header `Cache-Control` is defined for both directions. On a response it is the origin's statement of policy: how long this may be reused, by whom, and under what conditions. On a request it is the client's statement of tolerance: how old an answer it is willing to accept and whether it will accept a network trip. A cache sitting between them must satisfy both constraints simultaneously. The asymmetry to internalise: a request directive can almost always make reuse *stricter*, and only in one narrow case (`max-stale`) can it loosen it — and even then, not past `must-revalidate` or `no-store`. A client cannot talk a cache into serving something the origin marked unservable. ## The directives **`no-cache`** on a request tells every cache along the path not to reuse a stored response without end-to-end revalidation. The stored copy is still valuable: revalidation usually yields `304 Not Modified` and the cache serves its bytes. This is exactly what a browser sends when the user presses reload. **`no-store`** on a request asks that neither this request nor its response be written to cache storage. It is rarely sent and unevenly honoured. **`max-age=N`** says the client will not accept a stored response whose current age exceeds N seconds. `max-age=0` therefore rejects everything stored and forces a revalidation — which is why it behaves much like request `no-cache`. The nuance: `no-cache` demands revalidation regardless of age, while `max-age=0` frames it as an age constraint; when combined with `max-stale` the two behave differently, since `max-age=0, max-stale=300` would permit a response up to five minutes stale. **`max-stale[=N]`** relaxes the requirement: the client accepts a response that is stale, by up to N seconds, or by any amount if the value is omitted. This is the one client-side loosening directive, and it is bounded — a cache must not serve stale in response to it if the stored response carried `must-revalidate` or `no-cache`. It is useful for tolerant background jobs, prefetching, and clients on flaky links. **`min-fresh=N`** is the opposite: the client wants a response that will *remain* fresh for at least N more seconds. The motivating case is a client about to lose connectivity or start a long operation, which does not want something that expires mid-way. **`only-if-cached`** instructs the cache to answer from storage or, if it has no suitable stored response, return `504 Gateway Timeout` without contacting the origin. It underpins offline-first patterns and lets a client probe cache contents without generating upstream traffic. In a browser this is one of the primitives behind cache-first fetch strategies. **`no-transform`** forbids intermediaries from altering the payload — recompressing images, minifying, rewriting media types. It exists in both directions and matters where an intermediary would otherwise "helpfully" degrade content. ## Reload semantics in browsers Understanding these explains browser behaviour that otherwise looks arbitrary. A **normal reload** sends `Cache-Control: max-age=0` (some browsers `no-cache`) for the document, and revalidates subresources — which is precisely the behaviour `immutable` on a response suppresses. A **hard reload** sends `Cache-Control: no-cache` (historically also `Pragma: no-cache`) and refuses stored responses for the document *and* its subresources, so everything is re-fetched. This is why "it works after a hard refresh" is a caching diagnosis, not a fix: it tells you the stored response differed from the origin's current one. ## Why this matters in debugging When a client claims it is seeing stale data, the request directives determine what is even possible. If the client sends no directives, the stored response's `max-age` fully governs reuse and the client cannot force freshness without changing the URL. If the client can send `no-cache`, you have a way to test the origin without waiting for expiry. And if a proxy is returning `504` for requests that clearly should reach the origin, an `only-if-cached` directive slipped in by a client library is a real, if uncommon, cause. One caution: request directives are honoured by caches, not by the origin. Sending `Cache-Control: no-cache` on a request does not tell the application to skip its own database or Redis cache — those are outside the HTTP caching model entirely, and treating the header as a general "bypass all caching" signal is a category error unless the application deliberately implements it.

  • Can a client use a request directive to read a response the origin marked `no-store`?
    No. Request directives govern how a cache may use what it has stored, and a `no-store` response was never stored in the first place. More generally, request directives can restrict reuse or, with max-stale, relax it within limits the origin allows; they cannot grant permissions the origin withheld.
  • What is the practical difference between request `no-cache` and request `max-age=0`?
    Both force the cache to check with the origin before reusing a stored response, so in isolation they behave alike. The difference shows when combined with `max-stale`: `max-age=0, max-stale=300` still permits a response up to five minutes stale, because the age constraint is relaxed, whereas `no-cache` demands revalidation regardless of any staleness allowance.
  • Why is "it works after a hard refresh" not a fix?
    A hard refresh sends `Cache-Control: no-cache` and re-fetches the document and its subresources, so it proves the stored copies differed from what the origin now serves. It changes nothing for the users who will not do it. The actual fix is on the response side: correct the freshness lifetime, add a validator, or change the URL so the old copy is never consulted again.

saying these in an interview costs you the question

  • Believing a request directive can override the origin's `no-store` or `private`
  • Thinking `Cache-Control: no-cache` on a request bypasses the application's own database or Redis caching
  • Expecting `only-if-cached` to fall through to the origin on a miss instead of returning 504
  • Confusing `max-stale` (accept older) with `min-fresh` (demand newer)
  • Treating `Pragma: no-cache` as a current, generally honoured mechanism

context