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?
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
| Load | Browser holds | Browser sends | Server replies | Page shows | Step |
|---|---|---|---|---|---|
| 1 (08:50) | nothing | plain GET | 200, body, no-cache, ETag "e-41" | the page; copy stored | 1 |
| 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
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

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?
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?
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?
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?
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?
Go deeper
Pro tips
- Check your origin's 304 path from Node, not from a browser. Node's built-in
fetchkeeps no HTTP cache, so anIf-None-Matchyou 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:
In a page it is different: a script that setsIf-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 bytesIf-None-Matchitself gets the raw 304. - 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-cachedirective does not prevent the storing of responses but instead prevents the reuse of responses without revalidation"; heuristic caching (about 10%); cache-busted subresources withimmutableandno-cacheHTML 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 tosd-cdn-edge-caching, and browserstale-while-revalidateis left for the Depth pass.
Related lessons
Checked against the tools and docs as of 2026-09-27.