skip to content

questions

23

Explain the difference between the HTTP response headers `Cache-Control: no-cache`, `Cache-Control: no-store` and `Cache-Control: must-revalidate`, and give a case where each is the right choice.

level: juniorimportance: must knowfreq 78%

answer

  1. store vs reuse: two different steps
  2. no-store = never write it down
  3. no-cache = stored, but ask first (304 is cheap)
  4. must-revalidate only bites after expiry → 504 not stale
  5. Pragma: no-cache is a deprecated HTTP/1.0 relic

basics

~20 s

no-store means never write this to any cache. no-cache means you may store it but must check with the origin before every reuse. must-revalidate means you may serve it freely while fresh, but once it expires you must revalidate rather than serve it stale. Use no-store for secrets, no-cache for always-current data, must-revalidate when stale answers are unacceptable.

solid answer

~60 s

They sit at three different points of the lifecycle. - **`no-store`** — do not write the response (or request) to any storage, shared or private. This is the privacy directive: bank statements, password-reset pages, anything you do not want on disk or in a proxy. - **`no-cache`** — storing is fine, *reuse* is not, until the cache revalidates with the origin. A 304 makes reuse legal and cheap. Use it for content that must always be current but rarely changes, such as an HTML shell you want re-checked on every navigation — it saves bandwidth without risking staleness. - **`must-revalidate`** — only bites once the response is already stale. It forbids the cache from serving a stale copy when the origin is unreachable; the cache must return an error, typically 504, instead. Use it where a wrong answer is worse than no answer, such as an account balance. The classic mistake is reading `no-cache` as "do not cache". It means "do not reuse without asking". `no-store` is the one that means do not cache.

code

http · 11 lines
http
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Type: text/html

HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "a1b2c3"

HTTP/1.1 200 OK
Cache-Control: max-age=600, must-revalidate
ETag: "d4e5f6"

go deeper

for a junior

Nail the one-liners: no-store = never save it, no-cache = save it but ask before reusing, must-revalidate = no stale after expiry.

for a middle

Explain the store-versus-reuse split and note that no-cache still saves bandwidth via 304 responses.

for a senior

Discuss the failure modes — sensitive data left stored under no-cache, the 504 behaviour of must-revalidate during an origin outage, and the latency cost of no-cache on assets.

for a principal

Frame it as a policy decision per response class and set defaults at the edge so individual services cannot accidentally leak or over-revalidate.

## A cache does two separable things An HTTP cache **stores** a response and later **reuses** it. Almost all confusion around these directives comes from collapsing those two steps. `no-store` targets storing; `no-cache` targets reusing; `must-revalidate` targets reusing *after expiry*. ## no-store `Cache-Control: no-store` instructs every cache — the browser's disk cache, a corporate proxy, a CDN, an intermediate reverse proxy — not to write any part of the request or response to persistent or non-volatile storage. It is the strongest directive and the only one that is really about confidentiality. Send it on responses whose contents must not survive the transaction: financial detail pages, password reset flows, one-time codes, anything with a session-bound secret in the body. Two caveats. First, `no-store` is not a security control on its own — it is a request to well-behaved caches, and it does nothing about screenshots, swap files or logs. Second, it does not imply `private`, and pairing `no-store` with the belief that the data is now protected end to end is a common over-read; TLS and authorization do the actual protecting. ## no-cache `Cache-Control: no-cache` permits storage but requires **successful revalidation with the origin server before any reuse**. The stored copy is not wasted: on the next request the cache issues a conditional request, and if the origin answers `304 Not Modified` the cache serves its copy without re-downloading the body. So `no-cache` costs you a round trip but saves the payload — which is exactly what you want for a resource that must never be stale but usually has not changed, such as the HTML document that references fingerprinted assets. RFC 9111 also allows the qualified form `no-cache="Set-Cookie"`, meaning shared caches must strip the named fields before reusing the stored response rather than revalidate the whole thing. Support is thin and it applies only to shared caches. ## must-revalidate `Cache-Control: must-revalidate` does nothing while a response is fresh — that is the point people miss. `max-age=600, must-revalidate` serves from cache for ten minutes with no origin contact at all. Its effect starts at expiry: normally a cache is *allowed* to serve a stale response in some circumstances (notably when the origin is unreachable), and `must-revalidate` removes that permission. If revalidation cannot be completed, the cache must generate an error — `504 Gateway Timeout` — rather than hand back stale content. `proxy-revalidate` is the same rule restricted to shared caches, leaving private browser caches free to serve stale. ## Putting them together - `no-store` — sensitive, must not persist anywhere. - `no-cache` — must be current on every use; revalidation keeps it cheap. - `max-age=N, must-revalidate` — cheap for N seconds, then strictly correct or an error. - `max-age=N` alone — cheap for N seconds, then revalidate, with the cache permitted to fall back to stale if the origin is down. The combination `no-cache, no-store, must-revalidate` appears in countless codebases as a cargo-cult "do not cache" string. It is not wrong, merely redundant: `no-store` already forbids storage, so the other two have nothing to act on. `Pragma: no-cache` is often bolted on too; it is an HTTP/1.0 request-header artefact, ignored as a response header by modern caches, and RFC 9111 deprecates it. ## Where a wrong choice hurts Marking an API response `no-cache` when you meant `no-store` leaves sensitive JSON sitting in a shared proxy's storage — legal to store, merely revalidated before reuse. Marking a static asset `no-cache` when you meant a long `max-age` costs a conditional request on every page load, which on a high-latency mobile connection is far more expensive than the bytes you saved. And relying on `must-revalidate` to keep content current while it is still fresh does nothing at all — you need a shorter `max-age`.

  • Does `no-cache` prevent the response from being written to disk?
    No. It explicitly permits storage and only forbids reuse without revalidating against the origin first. A shared proxy or browser cache may keep the bytes on disk indefinitely under `no-cache`. If the concern is the data existing at rest, `no-store` is the directive you need.
  • If a response is `max-age=600, must-revalidate` and the origin is unreachable at second 700, what does the cache do?
    It must not serve the stale copy. `must-revalidate` removes the cache's normal permission to serve stale content when it cannot reach the origin, so it generates an error response, conventionally 504 Gateway Timeout. Without `must-revalidate`, the cache would be allowed to serve the stale copy instead.
  • Is `Cache-Control: no-cache, no-store, must-revalidate` wrong?
    Not wrong, just redundant. `no-store` already forbids storing the response, so there is nothing left for `no-cache` or `must-revalidate` to govern. The string persists as a copy-paste defensive incantation aimed at very old caches; on modern caches `no-store` alone is sufficient.

saying these in an interview costs you the question

  • Reading `no-cache` as "do not cache" — it means "do not reuse without revalidating"
  • Believing `must-revalidate` forces a check on every request while the response is still fresh
  • Using `no-cache` for sensitive data that must not be written to disk
  • Adding `Pragma: no-cache` as a response header and expecting modern caches to honour it
  • Assuming `no-store` gives any confidentiality guarantee beyond a request to cooperating caches

context

open as a page

In HTTP caching, what does it mean for a stored response to be fresh versus stale, and what is a cache allowed to do once a stored response goes stale?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A stored response is fresh while its age is below its freshness lifetime; it can be reused with no network trip. Once age exceeds that lifetime it is stale — which does not mean deleted. The cache normally revalidates with the origin, and a successful check makes the copy usable again with a reset clock.

open as a page

Two clients read the same HTTP resource, each edits it, and each sends a PUT. Both get 200 OK, but one client's edit has vanished. What happened, and what does HTTP offer to prevent it?

level: juniorimportance: must knowfreq 55%

basics

~20 s

That is the lost update problem: the second PUT was computed from stale data and overwrote the first. HTTP prevents it with a conditional write - the client echoes the ETag it read in an If-Match header, and the server answers 412 Precondition Failed on mismatch.

open as a page

In HTTP caching, what is the difference between a private cache and a shared cache, and what do the Cache-Control response directives private and public each tell them?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A private cache serves one user - the browser's own store. A shared cache serves many - a CDN, proxy or gateway. Cache-Control: private forbids shared caches from storing the response; public explicitly permits storing even responses that would otherwise be ineligible, such as authenticated ones.

open as a page

Walk through how an HTTP ETag and the If-None-Match request header produce a 304 Not Modified response, and what a client is expected to do when it receives one.

level: juniorimportance: must knowfreq 68%

basics

~20 s

The server sends an ETag identifying the representation. On the next request the client sends If-None-Match with that value; if it still matches, the server replies 304 Not Modified with headers but no body, and the client reuses its stored copy. Only the round trip is spent, not the payload.

open as a page

A cache normally stores a response under the request URL. What does the HTTP response header Vary add to that, and how does a shared cache use it when a later request arrives for the same URL?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Vary is a response header listing the request headers the server used to choose this response. The cache keys the entry on the URL plus those header values, so it reuses the stored copy only when a new request has matching values.

open as a page

What do the HTTP directives `Cache-Control: private`, `Cache-Control: public` and `Cache-Control: s-maxage` control, and what goes wrong if a per-user response is served without `private`?

level: middleimportance: must knowfreq 62%

basics

~20 s

private restricts storage to a single-user cache (the browser); shared caches such as CDNs and proxies must not store it. public allows shared storage even when rules would normally forbid it. s-maxage sets a freshness lifetime that only shared caches use, overriding max-age. Omitting private on per-user content lets a CDN serve one user's data to everyone.

open as a page

Explain what the HTTP directive Cache-Control: s-maxage does that max-age does not, and how CDN-Cache-Control and Surrogate-Control let you give a CDN a different lifetime from the browser.

level: middleimportance: must knowfreq 50%

basics

~20 s

s-maxage sets the freshness lifetime for shared caches only and overrides max-age there; browsers ignore it. CDN-Cache-Control and Surrogate-Control go further: they target the CDN specifically and are consumed by it, so browser, CDN and origin tiers can be tuned independently.

open as a page

What is the difference between a strong HTTP ETag and a weak one written as W/"abc", and where does that distinction actually change server behaviour?

level: middleimportance: must knowfreq 45%

basics

~20 s

A strong ETag promises byte-for-byte identity of the representation; W/ marks a weak one promising only semantic equivalence. Conditional GET uses weak comparison, so both produce 304. Range requests and write preconditions like If-Match use strong comparison, where a weak tag never matches.

open as a page

A server compresses responses when the request carries Accept-Encoding: gzip. Why must it also send Vary: Accept-Encoding, and what concretely goes wrong in a shared cache if it does not?

level: middleimportance: must knowfreq 52%

basics

~20 s

Compression makes one URL produce two different bodies. Without Vary: Accept-Encoding a cache stores one of them and serves it to everyone, so a client that cannot decompress may receive gzip bytes, or a gzip-capable client gets an uncompressed body.

open as a page

An HTTP response arrives at your cache with `Date: 10:00:00`, `Cache-Control: max-age=300` and `Age: 240`. How long can your cache serve it, and how is a stored response's current age actually computed?

level: middleimportance: should knowfreq 38%

basics

~20 s

Only about 60 more seconds. The Age header says the response is already 240 seconds old, and max-age=300 counts from origin generation, not from arrival. Current age = the age it arrived with (from Age or Date) plus the time it has since sat in your cache.

open as a page

HTTP defines both the `Expires` header and `Cache-Control: max-age`. If a response carries both, which one wins, and why does `Expires: 0` appear so often in real responses?

level: middleimportance: should knowfreq 42%

basics

~20 s

Cache-Control: max-age wins for any cache that understands HTTP/1.1 — essentially all of them. Expires is the HTTP/1.0 absolute-timestamp form and survives only as a fallback. Expires: 0 is an invalid date, which caches must treat as already expired, so it is a compact "already stale" marker for ancient caches.

open as a page

On an HTTP PUT or DELETE you can guard the write with either the If-Match header or the If-Unmodified-Since header. How do they differ, and when is each one safe to rely on?

level: middleimportance: should knowfreq 30%

basics

~20 s

If-Match compares an opaque ETag, so any change is detected exactly. If-Unmodified-Since compares an HTTP date with one-second resolution, so two edits inside the same second look unchanged and a stale write slips through. Prefer If-Match; use the date only when no ETag exists.

open as a page

For build-fingerprinted static assets such as `/static/app.8f3a1c.js`, teams often send `Cache-Control: public, max-age=31536000, immutable`. Explain what `immutable` adds on top of the long `max-age`, and what breaks if you use that header on a non-fingerprinted URL.

level: seniorimportance: should knowfreq 44%

basics

~20 s

The long max-age already stops normal re-fetching. immutable (RFC 8246) additionally tells the browser not to send a conditional revalidation request when the user reloads the page — the case where browsers otherwise revalidate anyway. On a non-fingerprinted URL it is unsafe: clients can pin the old bytes for a year with no way to purge them.

open as a page

What do the HTTP response directives `Cache-Control: stale-while-revalidate=60` and `Cache-Control: stale-if-error=86400` instruct a cache to do, and what risk does each accept in exchange for what benefit?

level: seniorimportance: should knowfreq 34%

basics

~20 s

stale-while-revalidate=60 lets a cache serve an expired response immediately for up to 60 more seconds while it refreshes in the background, so no user waits on the origin. stale-if-error=86400 lets it serve an expired response for up to a day when revalidation fails or the origin returns 5xx. Both trade bounded staleness for latency and availability.

open as a page

A backend returns HTML with a `Last-Modified` header but no `Cache-Control` and no `Expires`. Users report seeing outdated pages. Explain what caches are permitted to do with such a response and how you would fix it.

level: seniorimportance: should knowfreq 30%

basics

~20 s

With no explicit lifetime, a cache may invent one heuristically — commonly ten percent of the time since Last-Modified. A page unchanged for a hundred days can therefore be cached for ten. The fix is to always send an explicit Cache-Control, such as no-cache for content that must be current.

open as a page

What does HTTP status 428 Precondition Required mean, and when should a server return it rather than processing the request?

level: seniorimportance: should knowfreq 32%

basics

~20 s

428 means the request was refused because it carried no precondition. A server returns it on unconditional PUT/PATCH/DELETE when it wants to force clients to send If-Match, so nobody can blindly overwrite state. The response should explain which precondition is expected.

open as a page

Users report that a CDN is still serving an old version of a page hours after it was updated. How do you confirm which cache tier is serving the stale copy, and how do purging and revalidation differ as fixes?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Read the Age header and the CDN's cache-status header: a large Age means the edge is serving a stored copy, and Age accumulates across tiers. Purge actively evicts the edge copy now; revalidation only makes the edge ask the origin once its entry goes stale. Browser copies must expire on their own.

open as a page

An HTTP resource is served with only a Last-Modified header and clients revalidate with If-Modified-Since. What can go wrong, and how do you decide which validator to emit?

level: seniorimportance: should knowfreq 36%

basics

~20 s

HTTP dates resolve to one second, so a change in the same second a client fetched is invisible and the client keeps stale content indefinitely. Timestamps also move on deploys or restores without content changing. Emit an ETag derived from content or a version, keeping Last-Modified alongside it.

open as a page

A service reflects the request's Origin header into the Access-Control-Allow-Origin response header, and a CDN caches those responses. Browsers on some sites start reporting CORS failures while others receive a permissive value they should not. What is missing, and why does it produce exactly this symptom?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The response is missing Vary: Origin. Since the body and headers depend on the request's Origin, the cache stores one copy with one site's origin baked in and replays it to requests from other origins, so those browsers see a mismatched Access-Control-Allow-Origin.

open as a page

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%

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.

open as a page

You run a CDN in front of an API and the hit rate is poor because responses declare several request headers in the HTTP Vary header. How would you decide which headers belong in the cache key, and how would you normalize them?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Keep only headers that actually change the bytes, and collapse each to the smallest set of values the origin can really produce. Anything high-cardinality gets normalized to a derived low-cardinality header, or the variation moves into the URL instead.

open as a page