A caching strategy
Decide how long to cache HTML, assets, and API responses, and how to change them safely. A shop with a page, a stylesheet and a price API, recorded in Chrome over a real round trip, straight to the server and through nginx as a CDN: three policies, stale-while-revalidate, stale-if-error, private and public, a deploy, and Clear-Site-Data.
- 01Give each kind of response its own ruleCheck the page on every visit, keep files with a hash in their name for a year, and let data be as old as it safely can.
- 02Use old copies on purposeShow a stale copy while a new one is fetched, and keep a page working when the server fails, once you've checked what your CDN does.
- 03Change things safelyKeep personal pages out of shared caches, refresh the CDN as a step of every deploy, and clear a browser's cache at logout.
One shop, three kinds of response
Ana opens the Mint tea shop's page. It shows a heading, a line that greets her, and the price of Moroccan mint tea: £4.50. A moment later she opens it again. This time it appears almost at once.
Then the shop changes. It raises the price to £5.25 and gives its heading a new colour. Putting a new version of a site live like this is called a deploy. Ana comes back once more. Does she see the new price?
The course built this shop and timed it in Chrome. First, the shop told browsers to keep every response for an hour:
// The shop told browsers to keep every response for an hour: max-age=3600.// paintedAt is when the first words appeared, in ms (the median of 5 runs).const secondVisit = { paintedAt: 36, price: "4.50" };const afterDeploy = { price: "4.50", design: "old" }; console.log(`Second visit: painted at ${secondVisit.paintedAt} ms, showing £${secondVisit.price}.`);console.log(`After the deploy: £${afterDeploy.price}, with the ${afterDeploy.design} design.`);- Line 1 says what the shop sent.
max-age=3600means a copy stays fresh for 3,600 seconds, one hour. While a copy is fresh, the browser uses it without asking. - Line 2 explains
paintedAt: the moment the first words appeared on screen. That moment is called the first contentful paint, or FCP. It's counted in milliseconds (ms) from the start of the visit. - Like every time in this lesson, 36 is the median of 5 runs: the middle value, once they're sorted.
- Line 3 holds Ana's second visit. Line 4 holds her visit after the deploy.
- Lines 6 and 7 print
Second visit: painted at 36 ms, showing £4.50.andAfter the deploy: £4.50, with the old design.
Here are Ana's three visits side by side, next to the page the shop sent after the deploy:
The second visit painted at 36 ms, less than a twentieth of a second. The visit after the deploy was wrong: the price had gone up, but Ana still saw £4.50. Both results have one cause. For that hour, her browser didn't ask the shop anything. The shop's log for both visits is empty.
So each time a visitor comes back, two things matter for every response:
- How fast can they get it? Fastest is a copy from their own cache, with no question to the shop.
- How soon do they see a change? Soonest is asking the shop every time.
The two pull in opposite directions. A caching strategy is your answer to both, for each kind of response your site sends. HTTP caching in depth explained each header on its own. This lesson puts them together for one whole shop. For the same problem across a whole system, see Cache invalidation in the system design course.
How the course timed it
- The shop lives at
shop.test. Its page is HTML, and it links one stylesheet. A small script on the page fetches the price from an API: an address that answers with data instead of a page. - So every visit needs three responses: the page, the stylesheet and the price.
- The shop's own server is called the origin. It takes 200 ms to build the page or to look up the price.
- A small program between Chrome and the servers held every message on its way. So each round trip, a message there and its answer back, took 100 ms.
- Some visits went straight to the origin. Others went through nginx, a real web server, set up as a CDN. A CDN, a content delivery network, sits between visitors and the origin. It keeps copies of responses and hands them to every visitor who asks.
- Chrome made each set of visits 5 times. Every run started with nothing kept anywhere. Chrome waited 2 seconds between visits.
- Except in the section on
private, nobody is logged in, so the page greets each visitor as "guest".
So copies can sit in two kinds of cache. Each visitor's browser keeps its own, for that visitor only. The CDN keeps one set, shared by every visitor who comes through it.
Different kinds of response change in different ways, so they need different rules. A bookshop already works like this:
A bookshop has a sign on its door with this week's opening hours. The hours can change at any time, so regulars read the sign every time they arrive. The books inside are different. Each edition has its number printed on the cover. The shop can keep the second edition on its shelf for years, because a third edition would come with a new number.
- In real life: The sign on the door, read on every visit
- In HTTP caching: The page, sent with
no-cache: checked with the shop on every visit - In real life: The edition number printed on a book's cover
- In HTTP caching: A hash in a file's name, like
main.e6141c36.css - In real life: An edition that stays on the shelf for years
- In HTTP caching:
max-age=31536000, immutable: keep the file for a year - In real life: A new edition, with a new number
- In HTTP caching: A changed file, with a new name
A caching strategy gives each kind of response its own rule. Check the page on every visit, so a deploy shows at once. Keep files with a hash in their name for a year, because a change gives them a new name. Let data be as old as your visitors can accept. Keep anything personal out of shared caches. And make clearing the CDN a step of every deploy.
Every visit in this lesson is in this playground. Pick a rule the shop used, then a visit. The rows show what Ana saw, and which servers were asked.
- GreetingHello, guest
- Price£4.50, shown at 827.3 ms
- Designold
- Origin/ 304
- Origin/api/price 304
The shop sent Cache-Control: no-cache with the page, max-age=31536000, immutable with the stylesheet, and no-cache with the price. Visits went straight to the shop. In all 5 runs, the page showed the same thing and the servers were asked the same things. Times are the median of the 5 runs.
HTML vs versioned assets vs API data
Keeping everything for an hour was fast, and wrong. The course tried two other rules on the same visits: check everything, every time, and a different rule for each kind of response.
Check everything, every time
no-cache doesn't mean "don't keep it". It means: keep it, but ask the shop before each use. HTTP caching in depth compared that to phoning the shop to check. If nothing has changed, the shop answers 304 Not Modified: a short reply with no body. The browser then uses its own copy.
// Now the shop sends no-cache with every response: "ask me before you use your copy".// paintedAt is when the first words appeared, in ms (the median of 5 runs).const secondVisit = { paintedAt: 636, price: "4.50" };const afterDeploy = { price: "5.25", design: "new" }; console.log(`Second visit: painted at ${secondVisit.paintedAt} ms, showing £${secondVisit.price}.`);console.log(`After the deploy: £${afterDeploy.price}, with the ${afterDeploy.design} design.`);- Line 1 gives the new rule:
no-cacheon the page, the stylesheet and the price. - Lines 3 and 4 hold the same two visits as before.
- It prints
Second visit: painted at 636 ms, showing £4.50.andAfter the deploy: £5.25, with the new design.
After the deploy, Ana saw the new price and the new design at once. But her second visit painted at 636 ms, not 36. On that visit the shop was asked three times, once for each response. It answered 304 every time.
No file was downloaded again, yet each question needed a round trip. The page's question also waited while the shop built the page. This shop needs 200 ms for that, even to answer 304: it builds the page to see whether it changed.
A rule for each kind of response
Now the mixed rule. The page and the price are still checked on every visit. The stylesheet is different: its name holds a hash, and it's kept for a year.
// The page and the price send no-cache. The stylesheet has a hash in its name,// and sends max-age=31536000, immutable: "keep me for a year, and never ask".const secondVisit = { paintedAt: 548, price: "4.50" };const afterDeploy = { price: "5.25", design: "new" }; console.log(`Second visit: painted at ${secondVisit.paintedAt} ms, showing £${secondVisit.price}.`);console.log(`After the deploy: £${afterDeploy.price}, with the ${afterDeploy.design} design.`);- Lines 1 and 2 give the rules. The stylesheet sends
max-age=31536000, immutable. 31,536,000 seconds is 365 days. immutablesays the file won't change while it's fresh. It tells the browser not to check it, even on a reload.- Lines 3 and 4 hold the same two visits.
- It prints
Second visit: painted at 548 ms, showing £4.50.andAfter the deploy: £5.25, with the new design.
This time the shop was asked twice on the second visit: for the page and for the price. The stylesheet came from Ana's cache with no question at all. Her page painted at 548 ms, 88 ms sooner than when everything was checked.
Those 88 ms are close to one 100 ms round trip: the stylesheet's question. The browser doesn't paint until it has the stylesheet, as Render-blocking and parser-blocking resources showed. So that one round trip held back the paint.
And after the deploy, Ana saw the new price and the new design. Here are all three rules side by side:
The mixed rule was always up to date, like checking everything. And it skipped the one question that held back the first paint. web.dev suggests the same starting point: no-cache "for resources that should be revalidated with the server before every use", and max-age=31536000 "for versioned resources."
Why the page is checked on every visit
The page is the bookshop's door sign. Two things make it different from every other file:
- Its address can't change. People type it, and links point at it. The shop's page is
/, before and after every deploy. - It names every other file the visit needs. An old copy of the page would ask for the old stylesheet, too.
So the page has to be checked every time. An ETag keeps that check cheap. It's a short label the shop puts on each version, like the batch number on a milk carton in HTTP caching in depth. If Ana's copy has the current label, the answer is a 304.
Why a hashed file can be kept for a year
A hash is a short code worked out from a file's content. Change one character in the file, and the hash changes. Build tools put it into the file's name. webpack calls that name part [contenthash], and its docs say: "When the asset's content changes, [contenthash] will change as well."
The shop's stylesheet worked the same way. Before the deploy, it was called main.e6141c36.css. The deploy changed one colour in it, and the new file was main.e29a8f41.css. The new page linked the new name. Ana's browser had never seen that name, so it fetched it. Nobody asked for the old name again.
That's the edition number on a book's cover. A file at one name never changes, so it can sit in the cache for a year. MDN calls this cache busting: "new content should have a new URL, and caches aren't reused." It only works because the page, which names the files, is checked on every visit.
One change, one new file
The hash comes from the whole file. That matters for JavaScript. Suppose a site keeps all of it in one file: its own code and the libraries it uses. Fix one typo in its own code, and the whole file gets a new name. Every returning visitor downloads all of it again, libraries too.
A café keeps its recipes in a bound book. To fix one recipe, it has to print a whole new book. The café next door keeps its recipes in a ring binder. To fix one, it prints a single page and clips it in. Every other page stays where it was.
- In real life: A recipe book, bound in one piece
- In HTTP caching: One JavaScript file for the whole site
- In real life: Printing a whole new book to fix one recipe
- In HTTP caching: One change gives the whole file a new hash, so everyone downloads all of it
- In real life: The pages of a ring binder
- In HTTP caching: Several files, each with its own hash
- In real life: Printing one new page and clipping it in
- In HTTP caching: Only the changed file gets a new name
webpack's caching guide suggests moving libraries "such as lodash or react" into a file of their own, "as they are less likely to change than our local source code."
And the price?
The price is data from an API. Like the page, it lives at one fixed address, /api/price, so it can't get a new name when it changes. With no-cache, Ana always saw the right price, for one question per visit.
So for API data, ask: how old may the answer be? If it must be right, use no-cache. If a slightly old answer is fine, the next section has a faster choice.
stale-while-revalidate and stale-if-error
A stored copy is fresh until its max-age runs out, like milk before its best-before date. After that, it's stale. Normally a browser won't use a stale copy until it has checked with the server.
Each instruction inside a Cache-Control header, like max-age=60, is called a directive. Two more directives let a cache use a stale copy on purpose:
stale-while-revalidate: use the stale copy now, and check with the server in the background.stale-if-error: use the stale copy if the server fails.
Both come from RFC 5861, a short standard about stale copies.
Old now, new next time
A café keeps a newspaper on the counter for its customers. When you sit down, you read the copy that's there, even if it's an hour old. Meanwhile, the owner sends someone out for the latest edition. The next customer reads the new one.
- In real life: Reading the paper that's already on the counter
- In HTTP caching: Using the stale copy at once
- In real life: Someone fetching the latest edition meanwhile
- In HTTP caching: Checking with the server in the background
- In real life: The next customer reading the new edition
- In HTTP caching: The next visit getting the new response
- In real life: An hour old is fine; a week old isn't
- In HTTP caching:
stale-while-revalidate=60: up to 60 seconds past stale
That's stale-while-revalidate. RFC 5861 says it lets a cache "immediately return a stale response while it revalidates it in the background." To revalidate is to ask the server whether a copy is still good. Doing it in the background hides the wait "both in the network and on the server" from the visitor.
The shop sent max-age=1, stale-while-revalidate=60 with the price. Read it as a timeline:
- For 1 second, the copy is fresh. The browser uses it and asks nobody.
- From 1 to 61 seconds, it's stale, but the browser still uses it at once. At the same time, it asks the shop in the background.
- After 61 seconds, it's too old. In web.dev's words, "the stale response isn't used at all", and the page waits for the server as usual.
The course waited 2 seconds between visits, so the price was always stale by the next one. The shop raised the price just before Ana's second visit:
// The price sends max-age=1, stale-while-revalidate=60. It rose to £5.25 just before visit 2.const visits = [ { visit: "Second visit", price: "4.50", shownAt: 640.1, transferSize: 0 }, { visit: "Third visit", price: "5.25", shownAt: 524.4, transferSize: 0 },]; for (const { visit, price, shownAt, transferSize } of visits) { const from = transferSize === 0 ? "from the cache" : "after asking the shop"; console.log(`${visit}: £${price} at ${shownAt} ms, ${from}.`);}- Line 1 gives the rule, and says when the price changed.
- Lines 3 and 4 hold two visits.
shownAtis when the price appeared on the page, in ms: the median of 5 runs. transferSizecomes from Resource Timing, as in HTTP caching in depth. It's 0 when the page's request was answered from the cache.- Line 8 turns
transferSizeinto words. Line 9 prints one line per visit. - It prints
Second visit: £4.50 at 640.1 ms, from the cache.andThird visit: £5.25 at 524.4 ms, from the cache.
On the second visit, Ana saw the old price, at once. But the shop's log shows a request for the price on that same visit, answered 200. That was the check in the background. Its answer, £5.25, replaced the copy in her cache.
So the third visit showed £5.25, again straight from the cache. Its own background check got a 304, because the price hadn't changed again.
A no-cache price can't do this: it has to wait for the shop before it appears. Exercise 1 puts the two side by side. The cost of stale-while-revalidate is one visit with the old answer. So use it where a slightly old answer does no harm, like a list of popular teas. Keep no-cache where an old answer is wrong.
When the server fails
stale-if-error is for a worse moment: the server is broken. RFC 5861 says it lets a cache return a stale response when there's an error, "rather than returning a 'hard' error." MDN lists what counts as an error: "any response with a status code of 500, 502, 503, or 504."
A 500 is the server saying "something went wrong on my side". Think of a station whose live departure board breaks. The printed timetable on the wall is older, but it still tells you when your train should leave.
Notice the standard's exact words: a stale copy "MAY be used". It's allowed, not required. So the course broke the price API before Ana's second visit, and watched who used the old copy. First, the API answered 500:
// The price sends max-age=1, stale-if-error=60. Then the price API breaks and answers 500.// What Ana's next visit showed, the same in all 5 runs.const shown = { "Straight to the shop": "unavailable", "Through the CDN": "unavailable",}; for (const [route, price] of Object.entries(shown)) console.log(`${route}: the price was ${price}.`);- Line 1 gives the rule, and how the API broke.
- Lines 3 to 6 hold what Ana saw: once straight from the shop, once through the CDN.
- It prints
Straight to the shop: the price was unavailable.andThrough the CDN: the price was unavailable.
Chrome showed the error. So did nginx. Its log says EXPIRED: its copy of the price was stale, so it asked the API, got the 500, and passed it on. Then the course tried two more setups through the CDN:
// Through the CDN again, in two more setups. What Ana saw, the same in all 5 runs.const shown = { "The API stopped answering": "4.50", "The API answered 500, and nginx was set to use old copies": "4.50",}; for (const [setup, price] of Object.entries(shown)) console.log(`${setup}: £${price}, the old copy.`);- Line 3 is an API that stopped answering at all: it dropped the connection.
- Line 4 is the same 500 as before, but this time nginx was set to use old copies on a 500.
- It prints
The API stopped answering: £4.50, the old copy.andThe API answered 500, and nginx was set to use old copies: £4.50, the old copy.
This time nginx's log says STALE: it gave Ana its old copy. nginx's docs say the header "permits using a stale cached response in case of an error." In the recording, a dropped connection counted as an error, and a 500 didn't.
For a 500, nginx needed its own setting, proxy_cache_use_stale. It "Determines in which cases a stale cached response can be used." The course turned it on for errors, timeouts and http_500.
So sending the header isn't enough. Check what your CDN does with it, and turn on its own setting if you need it.
Invalidation and deploys
Invalidation means making caches stop using a copy that's now wrong. With the rules so far, it happens by itself:
- The page is checked on every visit, so a deploy shows at once.
- A changed file gets a new name, so nobody asks for its old copy.
A CDN in the middle changes that. A site can let the CDN keep its pages for a while, to spare the origin work, while browsers still check every time. That's what s-maxage is for: the office fridge's own rule, from HTTP caching in depth. MDN: it "indicates how long the response remains fresh in a shared cache," and it "is ignored by private caches."
The shop sent max-age=0, s-maxage=60 with its page. Browsers treat the page as stale at once, so they check on every visit. The CDN may keep it for 60 seconds. Ana visited, so the CDN stored the page. Then the shop deployed, and Ben visited:
// The page sends max-age=0, s-maxage=60: browsers check it every visit, the CDN keeps it 60 s.// Ana visits. The shop deploys a new design and price. Then Ben visits. The same in all 5 runs.const ben = { design: "old", price: "5.25" }; console.log(`Ben saw the ${ben.design} design with the price £${ben.price}.`);- Line 1 gives the rule. Line 2 says what happened, in order.
- Line 3 holds what Ben saw.
- Line 5 prints
Ben saw the old design with the price £5.25.
Ben got two versions of the shop on one screen. The page came from the CDN's copy, made before the deploy: the CDN's log says HIT. That old page linked the old stylesheet, so the design was old. The price went on to the shop, so it was new.
A restaurant raises its prices and updates the till. But last week's menus are still lying on the tables. A customer orders from an old menu, and the till charges the new price. The fix is simple: collect the old menus as soon as the new ones are printed.
- In real life: The menus already on the tables
- In HTTP caching: The page the CDN kept, fresh for 60 seconds by
s-maxage=60 - In real life: The till, which knows the new prices
- In HTTP caching: The price, checked with the shop on every visit
- In real life: Ordering from an old menu, paying the new price
- In HTTP caching: The old design and the new price on one screen
- In real life: Collecting the old menus as the new ones go out
- In HTTP caching: Refreshing the page at the CDN as a step of the deploy
No header can collect the menus. MDN: "There are no cache directives for clearing already-stored responses from caches on intermediate servers." So the course added a last step to the deploy: one request that told nginx to skip its copy, fetch the page from the shop, and store the new one.
nginx calls that setting proxy_cache_bypass. It "Defines conditions under which the response will not be taken from a cache." Here is the same deploy with that last step:
// The same visits, but the deploy ends by refreshing the page at the CDN.// What Ben saw, the same in all 5 runs.const ben = { design: "new", price: "5.25" }; console.log(`Ben saw the ${ben.design} design with the price £${ben.price}.`);- Line 1 says what changed: the deploy now ends with the refresh.
- Line 5 prints
Ben saw the new design with the price £5.25.
Cloudflare, which runs a CDN, calls this a purge: telling the CDN to drop its copies. It lets you purge one address, every file with a tag, or everything, and it recommends purging by address. Whichever CDN you use, make this a step of every deploy, so nobody has to remember it.
Clearing one visitor's browser
The other end is one visitor's own browser. When Ana logs out, the shop may want her browser to forget what it kept. The Clear-Site-Data header asks for that. MDN: it tells the browser to remove "browsing data of certain types (cookies, storage, cache)" for the site.
With the value "cache", the browser removes its "locally cached data" for the site. It's like handing back a hotel key card: the room is cleared for whoever comes next.
// The stylesheet sends max-age=3600. Logging out sends Clear-Site-Data: "cache".const visits = [ { visit: "Before logging out", transferSize: 0, paintedAt: 544 }, { visit: "After logging out", transferSize: 405, paintedAt: 656 },]; for (const { visit, transferSize, paintedAt } of visits) { const stylesheet = transferSize === 0 ? "from the cache" : "downloaded again"; console.log(`${visit}: stylesheet ${stylesheet}, painted at ${paintedAt} ms.`);}- Line 1 gives the rules: the stylesheet is kept for an hour, and the logout page sends
Clear-Site-Data: "cache". - Line 3 is a visit before logging out. Line 4 is the next visit after it.
- Line 8 turns
transferSizeinto words. - It prints
Before logging out: stylesheet from the cache, painted at 544 ms.andAfter logging out: stylesheet downloaded again, painted at 656 ms.
After logging out, Ana's browser had to fetch the page, the stylesheet and the price again, as on a first visit. Her page painted at 656 ms, close to her first visit's 636 ms. That's what a clean start costs.
So use it to remove personal data at logout, not as a way to deploy. It only clears the browser that gets it: MDN notes it "will not affect shared or intermediate caches."
Which policy fits?
Each response below needs a rule. Ask three questions, in this order:
- Is it personal? Then it's
private. - Does its name change whenever its content does? Then keep it for a year.
- May it be a little out of date? Then use a short
max-agewithstale-while-revalidate.
Anything else gets no-cache, with an ETag. Sort each response by the rule it should get:
- The home page's HTML, the same for every visitor
- A price API where a minute-old answer would be wrong
- main.e6141c36.css
- A JavaScript chunk named by a hash of its content
- An exchange-rate widget that may be a minute behind
- A list of trending teas, updated every few minutes
- A page that greets the visitor by name
- An API response with the visitor's past orders
Sort each response by the Cache-Control it should get.
Common mix-ups
- "One rule for every file is simpler, and good enough." No. Keeping everything for an hour gave the fastest second visit, and the old price after a deploy.
- "A file kept for a year can't be updated." No. Give the new version a new name. The page, checked on every visit, links the new name at once.
- "stale-if-error protects you from any outage." No. Chrome showed the 500. So did nginx, until its own setting told it to use the old copy.
- "A cookie keeps a page out of the CDN." No. The CDN filed the page by its address, and Ben was greeted as Ana.
- "private means the page isn't cached." No. It keeps the page out of shared caches. Ana's own browser still kept her page.
- "Waiting for s-maxage to run out is a deploy plan." No. Until it ran out, Ben got the old design with the new price.
Several of these mix-ups are about one question: which cache may keep the page, and for how long? Read the table across. Each row is one rule the shop's page used.
| What the page sends | May Ana's browser keep it? | May the CDN keep it? |
|---|---|---|
no-cache | Yes, but it asks the shop before every use. | Yes, but it asks the shop before every use. |
public, max-age=60 | Yes, for 60 seconds. | Yes, for 60 seconds. It gives that copy to anyone who asks for the same address. |
private, max-age=60 | Yes, for 60 seconds. | No. Every request goes on to the shop. |
max-age=0, s-maxage=60 | Yes, but it's stale at once, so the browser asks on every visit. | Yes, for 60 seconds. |
Practice exercises
Answer first, then use the hints.
A third visit was added: a repeat visit where the price sent no-cache. What does the last line print?
// Two visits with a stale-while-revalidate price, and one where the price sent no-cache.
const visits = [
{ visit: "Second visit", price: "4.50", shownAt: 640.1, transferSize: 0 },
{ visit: "Third visit", price: "5.25", shownAt: 524.4, transferSize: 0 },
{ visit: "Repeat visit, no-cache price", price: "4.50", shownAt: 827.3, transferSize: 300 },
];
for (const { visit, price, shownAt, transferSize } of visits) {
const from = transferSize === 0 ? "from the cache" : "after asking the shop";
console.log(`${visit}: £${price} at ${shownAt} ms, ${from}.`);
}Repeat visit, no-cache price: £4.50 at 827.3 ms, after asking the shop. A no-cache price waits for the shop's 304 before it appears. That came about 190 ms later than the stale-while-revalidate price on the second visit.
Ana comes back to the greeting page under each rule. What does the second line print?
// Ana comes back to the greeting page under each rule. What she saw, the same in all 5 runs.
const ana = {
"public, max-age=60": { greeting: "Hello, Ana", cdn: "not asked" },
"private, max-age=60": { greeting: "Hello, Ana", cdn: "not asked" },
};
for (const [rule, { greeting, cdn }] of Object.entries(ana)) console.log(`${rule}: Ana saw "${greeting}". The CDN was ${cdn}.`);private, max-age=60: Ana saw "Hello, Ana". The CDN was not asked. Her own browser kept her page for 60 seconds, so it didn't need the CDN at all.
In the playground, with everything no-cache, what status did the origin send for the page, /, on the visit after the deploy?
304. The page hadn't changed: it still linked /main.css, because that stylesheet has no hash in its name. Only the stylesheet's content changed, so /main.css came back 200.
When the price API stopped answering, when did Ana's repeat visit show the price, in ms?
836 ms. The CDN waited for the API, saw the connection drop, and then served its old copy.
nginx can refresh an expired copy in the background while it gives visitors the stale one, like stale-while-revalidate. Which directive turns that on?
proxy_cache_background_update. nginx's docs say it "Allows starting a background subrequest to update an expired cache item, while a stale cached response is returned to the client."
Which Clear-Site-Data value asks the browser to clear every type of data for the site, even types added in future?
"*", the wildcard. MDN says it clears "all types of data for the origin of the response", and that types added in future "will also be covered by it."
Check your understanding
For each question, ask three things: who may keep the response, for how long, and what happens when it changes or the server fails.
Question 1 of 7What does the public example print?
Read the code, then predict// The page greets whoever's cookie it gets, and sends public, max-age=60. // Ana visits, then Ben, through the same CDN. What Ben saw, the same in all 5 runs. const ben = { greeting: "Hello, Ana", cdn: "HIT" }; console.log(`Ben saw "${ben.greeting}". The CDN's log said ${ben.cdn}.`);Choose an answer to see the explanation.
Question 2 of 7What does the second line of this stale-if-error example print?
Read the code, then predict// The price sends max-age=1, stale-if-error=60. Then the price API breaks and answers 500. // What Ana's next visit showed, the same in all 5 runs. const shown = { "Straight to the shop": "unavailable", "Through the CDN": "unavailable", }; for (const [route, price] of Object.entries(shown)) console.log(`${route}: the price was ${price}.`);Choose an answer to see the explanation.
Question 3 of 7The shop kept every response for an hour. Right after a deploy, what did a returning visitor see?
Choose an answer to see the explanation.
Question 4 of 7The price used stale-while-revalidate, and it changed just before a visit. What did that visit show?
Choose an answer to see the explanation.
Question 5 of 7Why must a page that greets the visitor by name send private?
Choose an answer to see the explanation.
Question 6 of 7A CDN kept the page for 60 seconds with s-maxage. After a deploy, visitors got the old design with the new price. What fixed it?
Choose an answer to see the explanation.
Question 7 of 7A site keeps all its JavaScript, its own code and its libraries, in one file with a hash in its name. One line of its own code changes. What does a returning visitor download?
Choose an answer to see the explanation.
Key takeaways
- Give each kind of response its own rule. One rule for everything is either slow or out of date.
- The page:
no-cachewith an ETag, so a deploy shows at once. - Files with a hash in their name:
max-age=31536000, immutable. A change gives them a new name. - Keep code that changes often apart from code that rarely does, so one change doesn't rename everything.
- API data:
no-cacheif it must be right, or a shortmax-agewithstale-while-revalidateif a slightly old answer is fine. - Send
stale-if-error, then check what your CDN does with it. Chrome didn't use it. - Anything personal is
private, even if it only reads a cookie. - If a CDN keeps your pages, purge them as a step of every deploy.
Clear-Site-Datais for one browser, at logout.
Remember the one-liner.
Each kind of response gets its own rule, anything personal stays private, and every deploy clears what the CDN keeps.
Sources
The facts on this page come from these standards and docs:
- Cache-Control header, MDN.
- Prevent unnecessary network requests with the HTTP Cache, web.dev.
- Caching, webpack.
- RFC 5861: HTTP Cache-Control Extensions for Stale Content, the IETF.
- Keeping things fresh with stale-while-revalidate, web.dev.
- Module ngx_http_proxy_module, nginx.
- RFC 9111: HTTP Caching, the IETF.
- Purge cache, Cloudflare.
- Clear-Site-Data header, MDN.
Coming next: The browser's caches. A browser has more than one cache: the memory cache, the HTTP cache, a service worker's cache, the code cache, and the back/forward cache. Which one served your file?