Skip to content

CDN and edge caching

The edge hands one copy to everyone whose request builds the same key. Decide what is shared, for how long, and how to retract it.

Predict first

Before you read: predict

ExamHall's exam start page, /exam/ECON-201/start, greets the candidate by name, read from the session cookie. The origin sends it with Cache-Control: public, max-age=30, s-maxage=60. The CDN caches whatever that header allows and uses its default cache key. It receives five requests for the page, one at a time: Amal at 09:00:00, Ben at 09:00:20, Amal again at 09:00:40, Chidi at 09:00:50 and Dana at 09:01:15.

How many of the five reach the origin?

Pick one

The 30-second version

An edge cache stores one copy per cache key: by default the method and full URL, query string included; cookies and headers count only if you add them. Everyone with that key gets that copy until s-maxage (else max-age) seconds after it was stored, then refetched. private or no-store keeps a response out. Purges or new URLs retire copies.

The edge hands one copy to everyone whose request builds the same key. Decide what is shared, for how long, and how to retract it.

Step through the code

Trace of the opening question

Play options

Ready: press Play or step with Next frame

Row 1 of 5 · Time 09:00:00

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

  • Time: 09:00:00
  • Request: Amal
  • Key: GET /exam/ECON-201/start
  • Stored copy: none: fetch, store "Amal"; lifetime 60 (s-maxage, not max-age)
  • Served: "Amal"

Rule · step 1

The edge builds a cache key from the method and the full URL, query string included. Headers and cookies join the key only if configured.

Rule · step 2

A shared cache takes its lifetime from s-maxage, else from max-age. With private or no-store it must not store the response.

Time
09:00:00
Request
Amal
Key
GET /exam/ECON-201/start
Stored copy
none: fetch, store "Amal"; lifetime 60 (s-maxage, not max-age)
Served
"Amal"
Step
1, 2
Trace of the opening question
TimeRequestKeyStored copyServedStep
09:00:00AmalGET /exam/ECON-201/startnone: fetch, store "Amal"; lifetime 60 (s-maxage, not max-age)"Amal"1, 2
09:00:20 (changed)Ben (changed)same key (changed)age 20 < 60: fresh (changed)"Amal"3, 5
09:00:40 (changed)Amal (changed)same keyage 40 < 60: fresh (changed)"Amal"2, 3
09:00:50 (changed)Chidi (changed)same keyage 50 < 60: fresh (changed)"Amal"3, 5
09:01:15 (changed)Dana (changed)same keyage 75: expired; fetch, store "Dana" (changed)"Dana" (changed)3

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

The edge builds a cache key from the method and the full URL, query string included. Headers and cookies join the key only if configured.
See it run
In the kitchen
  • one rack slot per leaflet title, whoever asks → the cache key (step 1)

Analogy: A network of tourist information offices

Head office prints leaflets; every town office keeps them on a rack and hands them to visitors. The rack has one slot per leaflet title, and each rack copy carries a "take off the rack at" time.

In the offices In the edge cache
one rack slot per leaflet title, whoever asks the cache key (step 1)
the rack's "take off at" time, separate from how long a visitor keeps theirs s-maxage (step 2)
a personal booking confirmation never goes on the rack private, no-store (step 2)
each town office stocks its own rack on its first request each PoP keeps its own copies (step 4)
head office recalls every leaflet that mentions route 5 purge by tag (step 7)
a reprint under a new title code, so old stock is simply never asked for a versioned URL (step 7)
a regional office that restocks the town offices an origin shield (step 8)

In real terms: a CDN hands a key's stored response to every request building that key while its shared-cache lifetime lasts.

Breaks down when: a clerk would notice a visitor's name printed on a leaflet; an edge never looks inside the body. Wrong inference: "the CDN will spot a personal page." It will not, which is why step 5 exists.

Contrasting case: a clerk who phones head office for every visitor. Nothing leaks, and head office carries all the load. That is no-store everywhere, or a key that includes the session.

Examples, run for real

Each script sets a virtual clock between requests, so every age is exact.

4.1 ExamHall: the start page that greeted the wrong candidate

EXH-688: candidates saw someone else's name on the exam start page. The shell was public, s-maxage=60 and printed the name from the cookie. MR !1147 split it: the shared shell stays at the edge, and the name comes from /api/me, sent as private, no-store.

BEFORE shell = public, s-maxage=60 and carries the name
  t= 0 Farah /exam/ECON-201/start MISS "ECON-201: Good luck, Farah"
  t=15 Gil   /exam/ECON-201/start HIT "ECON-201: Good luck, Farah"
  t=45 Hana  /exam/ECON-201/start HIT "ECON-201: Good luck, Farah"
AFTER  shell = public, s-maxage=60, no name · /api/me = private, no-store
  t= 0 Farah /exam/ECON-201/start MISS "ECON-201 shell" · /api/me MISS "Farah"
  t=15 Gil   /exam/ECON-201/start HIT "ECON-201 shell" · /api/me MISS "Gil"
  t=45 Hana  /exam/ECON-201/start HIT "ECON-201 shell" · /api/me MISS "Hana"
  origin: /exam/ECON-201/start 1, /api/me 3
ALT    shell as BEFORE, but the edge key includes the session cookie
  origin: /exam/ECON-201/start 3

The body differed by the cookie, the key did not (steps 1, 5). Keying on the cookie also stops the leak, but every candidate then misses; splitting keeps one shared copy.

4.2 ExamHall: notices going stale, then the origin failing

/api/exams/ECON-201/notices is sent with public, max-age=60, stale-while-revalidate=30, stale-if-error=600.

t=  0 200 MISS           body=v1   age=0   origin calls +1
t= 30 200 HIT            body=v1   age=30  origin calls +0
t= 65 notice v2 published
t= 70 200 STALE          body=v1   age=70  origin calls +1
t= 75 200 HIT            body=v2   age=5   origin calls +0
t=100 origin starts answering 503
t=140 200 STALE          body=v2   age=70  origin calls +1
t=200 200 STALE-IF-ERROR body=v2   age=130 origin calls +1
t=800 503 EXPIRED        body=down age=-   origin calls +1

At 70 the request gets v1 at once while the background fetch stores v2 (step 6; this edge always takes that permission). With s-maxage neither window would apply. At 200 only stale-if-error still applies, so the 503 is hidden. At 800 it is past 60 + 600 s and the error reaches the candidate.

4.3 ExamHall: an erratum on three pages

Question 4 appears on the exam page, the question page and the day's timetable. Each response carries a Surrogate-Key naming every exam it shows.

/exams/ECON-201           Surrogate-Key: exam-econ201
/exams/ECON-201/q/4       Surrogate-Key: exam-econ201 q-econ201-4
/timetable/2026-10-10     Surrogate-Key: day-2026-10-10 exam-econ201 exam-law110
purge URL /exams/ECON-201/q/4  removed 1 | /exams/ECON-201 HIT OLD · /exams/ECON-201/q/4 MISS new · /timetable/2026-10-10 HIT OLD
purge tag exam-econ201         removed 3 | /exams/ECON-201 MISS new · /exams/ECON-201/q/4 MISS new · /timetable/2026-10-10 MISS new
versioned paper JSON           v7 HIT "paper v7" · v8 MISS "paper v8" · v8 again HIT | origin calls for the paper 2

A URL purge removes one copy; the tag reaches every page that declared it (step 7). The paper JSON needs no purge: the new page links ?v=8, a new key.

4.4 Proof domain, ticketing: on-sale behind an origin shield

Event 42's seat map is public, s-maxage=60. The London, Paris and Frankfurt PoPs get their first request at 0, 10 and 50 s.

no shield        LHR@0 MISS age=0 · CDG@10 MISS age=0 · FRA@50 MISS age=0 | origin 3 | then FRA@65 HIT, origin 3
with shield      LHR@0 MISS age=0 · CDG@10 MISS age=10 · FRA@50 MISS age=50 | origin 1 | then FRA@65 EXPIRED, origin 2

With the shield, three PoP misses cost the origin one fetch (step 8). Frankfurt's copy arrived already 50 s old, so it expires at 60 s of total age, not 60 s after Frankfurt stored it.

Picture it

a flat icon on a pale background: a charcoal canopy, with lavender and lime panels, sheltering a small pale-cyan cube.
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 streaming app's /browse/rows is public, s-maxage=600. The CDN adds a device class to the cache key, reduced at the edge from User-Agent to mobile, tablet or desktop. There is no origin shield, and each viewer reaches the nearest PoP. Within one minute, one at a time, the rows are requested by a London iPhone, a London Windows laptop, a London Android phone, a Paris iPhone and a Paris Mac.

How many of the five requests reach the origin?

Pick one

Question 2 of 5

/products/881 was stored at the edge with Cache-Control: public, s-maxage=86400 and a price of £89. Ten minutes later the price drops to £79 in the database. Shoppers reach the page from bookmarks, search results and old emails.

Which one change makes that URL show £79 within a minute?

Pick one

Question 3 of 5

A chat app's sticker sheet /stickers/party-pack.png is public, s-maxage=86400 and warm at 4 PoPs that sit behind one origin shield. At t = 100 s the team purges that URL across the CDN. The four PoPs then get one request each, one after another, at t = 110, 111, 112 and 113 s.

How many requests reach the origin?

Pick one

Question 4 of 5

A ticket site shows event 51's start time on three pages, all public, s-maxage=3600: /events/51 and /events/51/seats, each sent with Cache-Tag: event-51, and the venue listing /venues/3/events, sent with Cache-Tag: venue-3,listing. The start moves from 19:30 to 20:00, and the team purges the tag event-51. One second later the event and seat pages show 20:00, but fans still see 19:30 on the listing.

Which is the bug?

Pick one

Question 5 of 5

A payments site publishes /rates/eur-gbp.json through a CDN with Cache-Control: public, max-age=60. Requests are rare: at t = 0, 500, 700, 800 and 950 s. The rate changes at t = 300, and the origin answers 503 from t = 600 to 900. The team wants two things: while the origin is up, nobody gets a copy older than 60 s; while it is down, everybody still gets a rate at most 15 minutes old.

Which one change to the header meets both?

Pick one

Go deeper

Pro tips
  1. Default authenticated routes to Cache-Control: private, no-store and opt shared ones in deliberately (curriculum pro tip). When: adding any route that reads a session or an Authorization header (steps 2, 5). Verify: with a stray public, max-age=30, Ben's request with his own bearer token gets Amal's response (ben HIT "amal"), because RFC 9111 §3.5 allows reuse when public is present; with private, no-store each gets their own. This is the model, not a vendor's CDN.
  2. Give the edge its own lifetime with CDN-Cache-Control (RFC 9213), for example CDN-Cache-Control: max-age=300 beside Cache-Control: no-cache. When: the edge may keep a copy longer than browsers should (steps 2, 7). Verify: a cache that implements RFC 9213 must ignore Cache-Control when a valid CDN-Cache-Control is present. Status: DOC (RFC 9213 §2.2); not run, and edge.mjs does not implement it.
Source
  • RFC 9111, HTTP Caching: https://www.rfc-editor.org/rfc/rfc9111 · fetched 2026-09-27. Valid as of 2026-09-27.
  • Both say "MAY" and "SHOULD", so a CDN may behave differently. Valid as of 2026-09-27.
  • RFC 9213, Targeted HTTP Cache Control (CDN-Cache-Control): https://www.rfc-editor.org/rfc/rfc9213 · fetched 2026-09-27 (pro tip 2). Valid as of 2026-09-27.
  • Cloudflare, Cache keys: https://developers.cloudflare.com/cache/how-to/cache-keys/ · fetched 2026-09-27. The default key is the full URL with its query string (plus a few headers such as Origin); cookies and other headers are not included by default; custom keys "may reduce your cache hit rate". Custom keys offer "Sort query string", and user features: "device_type classifies a request as mobile, desktop, or tablet based on the User Agent", and lang, the first language code from Accept-Language. Valid as of 2026-09-27.
  • Cloudflare, Origin Cache Control: https://developers.cloudflare.com/cache/concepts/cache-control/ · fetched 2026-09-27. Documents s-maxage, private, no-store, stale-while-revalidate (the first request after expiry receives stale content with an UPDATING status while revalidation runs in the background) and stale-if-error, under the heading "s-maxage disables stale-while-revalidate". It also says Cloudflare "does not consider vary values in caching decisions" by default, except Accept-Encoding, configured Vary rules and images. Valid as of 2026-09-27.
  • Cloudflare, Purge by cache-tags: https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/ · and Fastly, Working with surrogate keys: https://www.fastly.com/documentation/guides/full-site-delivery/purging/working-with-surrogate-keys/ · fetched 2026-09-27. Cloudflare reads a comma-separated Cache-Tag header and removes it before the response reaches visitors; Fastly reads a space-separated Surrogate-Key header, and purging a key purges every object associated with it. Valid as of 2026-09-27.
  • Cloudflare, Tiered Cache: https://developers.cloudflare.com/cache/how-to/tiered-cache/ · fetched 2026-09-27. A lower tier asks an upper tier; "only the upper-tier can ask the origin for content". Valid as of 2026-09-27.

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