Skip to content

HTTP caching headers

no-cache still stores; no-store never stores. Freshness decides whether the browser asks at all; validation decides whether it downloads again.

Predict first

Before you read: predict

ExamHall serves the ECON-201 exam page with Cache-Control: no-cache and ETag: "e-41". A candidate opens it at 08:50, goes to the lobby, and follows the lobby's link back to the exam page at 08:52. The page has not changed on the server.

What happens on the second load?

Pick one

The 30-second version

A browser stores a response unless it says no-store. While a stored copy is fresh (max-age, or a heuristic), it is reused with no request. Once it is stale, or always under no-cache, the browser validates first: it sends the ETag in If-None-Match, and a 304 means "reuse your stored body". Vary adds request headers to the key.

no-cache still stores; no-store never stores. Freshness decides whether the browser asks at all; validation decides whether it downloads again.

Step through the code

Trace of the opening question

Play options

Ready: press Play or step with Next frame

Row 1 of 2 · Load 1 (08:50)

Starting row · computed by comparing each row with the one before

  • Load: 1 (08:50)
  • Browser holds: nothing
  • Browser sends: plain GET
  • Server replies: 200, body, no-cache, ETag "e-41"
  • Page shows: the page; copy stored

Rule · step 1

For each response, the cache first decides whether it may store it: no-store forbids that; private forbids shared caches but allows the browser's own.

Load
1 (08:50)
Browser holds
nothing
Browser sends
plain GET
Server replies
200, body, no-cache, ETag "e-41"
Page shows
the page; copy stored
Step
1
Trace of the opening question
LoadBrowser holdsBrowser sendsServer repliesPage showsStep
1 (08:50)nothingplain GET200, body, no-cache, ETag "e-41"the page; copy stored1
2 (08:52) (changed)copy, no-cache (changed)If-None-Match: "e-41" (changed)304, no body (changed)the stored copy (changed)4, 5

Space plays or pauses · ←/→ one row · Home/End first/last

Rows come from the pack's own trace table.

How it works

Step 1 of 8

For each response, the cache first decides whether it may store it: no-store forbids that; private forbids shared caches but allows the browser's own.
See it run
In the kitchen
  • sealed orders marked "read, then destroy; never file" → no-store (step 1)
  • the captain's annotated chart stays in the cabin, never in the harbour's shared library → private (step 1)

Analogy: A ship's chart room

A navigator works from paper charts. Each chart carries a "valid until" stamp; before that date it is used without asking anyone. After it, or on charts stamped "confirm before every voyage", the navigator radios the hydrographic office with the chart's correction number. "Correction 41 is still the latest" means keep using the same paper; otherwise a new chart arrives.

In the chart room In HTTP
the "valid until" stamp freshness lifetime, max-age (step 2)
using the chart without radioing fresh copy reused, no request (step 3)
"confirm before every voyage" stamp no-cache (step 4)
radioing the correction number If-None-Match with the ETag (step 4)
"still the latest, keep your paper" 304, stored body reused (step 5)
sealed orders marked "read, then destroy; never file" no-store (step 1)
the captain's annotated chart stays in the cabin, never in the harbour's shared library private (step 1)
a metric chart and a fathom chart of one sea area: the unit you ask for picks the chart Vary (step 7)

In real terms: a stored response is reused silently while fresh; otherwise the browser asks with its validator, and a 304 lets it keep the body it has.

Breaks down when: a hydrographic office can broadcast a notice that a chart is withdrawn. An HTTP server cannot reach into browsers; a fresh copy stays in use until its lifetime ends. Wrong inference: "the server can invalidate my users' caches after a deploy." It cannot, which is why step 8 changes the URL instead.

Contrasting case: a navigator who throws every chart overboard after each voyage and buys a new one: that is no-store, not no-cache. Nothing is kept, so nothing can be confirmed.

Examples, run for real

Every example ran in Chromium against a local origin that logs each request. "No request" means the origin saw nothing.

4.1 ExamHall: three responses, three policies

The HIST-104 exam page loads a content-hashed chunk, and the chunk fetches the candidate's attempt. The candidate opens the exam, reads the instructions page, then follows its link back.

GET /exam/HIST-104                    Cache-Control: no-cache                        ETag: "e-7"
GET /_next/static/chunks/app.3f9a.js  Cache-Control: max-age=31536000, immutable
GET /api/attempts/a-7                 Cache-Control: private, no-store
visit 1  page shows "12 answers saved"
  HTML  /exam/HIST-104                     plain GET -> 200
  chunk /_next/static/chunks/app.3f9a.js   plain GET -> 200
  API   /api/attempts/a-7                  plain GET -> 200
visit 2 (link from the instructions)  page shows "12 answers saved"
  HTML  /exam/HIST-104                     If-None-Match "e-7" -> 304
  chunk /_next/static/chunks/app.3f9a.js   no request (served from cache)
  API   /api/attempts/a-7                  plain GET -> 200

The HTML is validated and costs a 304 with no body (steps 4–5). The chunk is fresh for a year, so no request is sent (step 3); a new build gets a new file name. The attempt holds a candidate's answers and is never stored (step 1).

4.2 ExamHall: the blank page after a deploy

EXH-648: a catch-all rule gave the exam HTML max-age=3600. Candidates who had opened the instructions before the 08:55 deploy returned and pressed Start, which imports the question-paper chunk. The deploy had replaced paper.3f9a.js with paper.7c21.js.

HTML max-age=3600  after the deploy: /_next/static/chunks/paper.3f9a.js -> 404
                 page shows "blank page: chunk failed to load"
HTML no-cache   after the deploy: /exam/ECON-201 If-None-Match "e-1" -> 200 · /_next/static/chunks/paper.7c21.js -> 200
                 page shows "question paper 7c21 loaded"

The old HTML was still fresh, so it was reused without a request (step 3) and named a deleted chunk. MR !962 switched the HTML to no-cache with an ETag: validation fails, the new HTML arrives with a 200 (step 6) and names the new chunk.

4.3 Proof domain, ticketing: a legend in the fan's language

A seat-map legend is served with max-age=300, Vary: Accept-Language and one ETag per language. The page asks in English, German, English, English.

  en: plain GET -> 200 (body sent); shows "Stalls · Circle · Balcony"
  de: If-None-Match "lg-en" -> 200 (body sent); shows "Parkett · Rang · Balkon"
  en: If-None-Match "lg-de" -> 200 (body sent); shows "Stalls · Circle · Balcony"
  en: no request; shows "Stalls · Circle · Balcony"

The German request cannot reuse the English copy (step 7). Chromium keeps one stored response per URL, so the German copy replaced the English one and English went to the server again. RFC 9111 allows several variants; that part is Chromium's choice.

Picture it

a pale cyan pantry jar with a thick charcoal outline and lid, and a blank lime label with two lavender half-circles, on an off-white background (opened 2026-09-27). It stands for a kept copy, and nothing more; the blank label carries no date and no validator.
An illustration to help it stick. The mechanism is in the sections above.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 5

A ticketing site's event page is no-cache with an ETag, and it loads /assets/seatmap.js, a file name without a hash, served with Cache-Control: max-age=31536000, immutable. Tonight's deploy changes that file. A fan who viewed the event yesterday opens it again this morning.

Which seat-map code runs?

Pick one

Question 2 of 5

/api/events/412/prices returns prices in the currency named by the request header X-Currency, with Cache-Control: private, max-age=300 and no ETag. A fan switches from EUR to GBP and still sees euro prices.

Which one change shows GBP prices after the switch and still sends no request when the fan asks for GBP again?

Pick one

Question 3 of 5

A box-office dashboard fetches /api/sales/today (Cache-Control: no-cache, an ETag) every 30 s with a plain fetch() and counts res.status === 304 as "unchanged". On the second fetch the server's log shows a 304.

What does the page's res show?

Pick one

Question 4 of 5

A ticketing site has four event pages, each served with a different set of headers. Prices on all four change at 18:00. Fans who viewed one page at 15:00 and return shortly after 18:00 still see the old prices, and the server's log shows no request from them for that page. On the other three pages, returning fans see the new prices.

Which headers did that page have?

Pick one

Question 5 of 5

/api/events/412/summary is a 400 KB seat summary served with Cache-Control: no-cache and ETag: "sum-1". At 18:00 sales close and the data freezes. The team wants each browser that revalidates after 18:00 to keep the full summary and send no request for the next 10 minutes, without the 400 KB being sent again.

What should the server reply to that revalidation?

Pick one

Go deeper

Pro tips
  1. Check your origin's 304 path from Node, not from a browser. Node's built-in fetch keeps no HTTP cache, so an If-None-Match you set reaches the server as written and its 304 comes back to you unchanged. When: after adding ETags to HTML or an API (steps 4–5). Verify:
    If-None-Match (none) -> 200, ETag "e-41", body 11 bytes
    If-None-Match "e-41" -> 304, ETag "e-41", body 0 bytes
    If-None-Match "e-40" -> 200, ETag "e-41", body 11 bytes
    In a page it is different: a script that sets If-None-Match itself gets the raw 304.
  2. Test caching with DevTools' "Disable cache" unchecked. While it is checked and DevTools is open, the browser skips its HTTP cache, so every request looks uncached. When: reading the Network panel's Size column ("(disk cache)", "(memory cache)") to confirm step 3. Status: DOC (Chrome DevTools, Network features reference: https://developer.chrome.com/docs/devtools/network/reference); not run in this session.
Source
  • Supports steps 1–7. Valid as of 2026-09-27.
  • RFC 9110, HTTP Semantics: https://www.rfc-editor.org/rfc/rfc9110 · §8.8.3 ETag, §13.1.2 If-None-Match, §15.4.5 304 Not Modified, §12.5.5 Vary. Supports steps 4–7. Valid as of 2026-09-27.
  • Supports step 8. Valid as of 2026-09-27.
  • Fetch Standard, HTTP-network-or-cache fetch: https://fetch.spec.whatwg.org/#http-network-or-cache-fetch · fetched 2026-09-27. If the forwarded response's status is 304 and a stored response exists, the stored response's headers are updated and "Set response to storedResponse"; a request whose header list already contains If-None-Match (or the other conditional headers) has its cache mode set to "no-store". Valid as of 2026-09-27.
  • MDN, HTTP caching: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching · fetched 2026-09-27. "The no-cache directive does not prevent the storing of responses but instead prevents the reuse of responses without revalidation"; heuristic caching (about 10%); cache-busted subresources with immutable and no-cache HTML with an ETag; Vary: Accept-Language; Chrome does not revalidate subresources on reload. Supports steps 1–8. Valid as of 2026-09-27.
  • RFC 5861 (stale-while-revalidate, stale-if-error), listed in curriculum-v2.json, is not taught in this Survey pack: its edge use belongs to sd-cdn-edge-caching, and browser stale-while-revalidate is left for the Depth pass.

Checked against the tools and docs as of 2026-09-27.