Copies close to every user
A bestselling book isn’t shipped from the publisher’s warehouse every time someone orders it. Local bookshops keep copies on their shelves; when they run out, they order more from the publisher. Readers get the book faster, and the publisher’s warehouse isn’t overwhelmed.
A CDN is a chain of local bookshops for your files. Its servers, called edges (or points of presence, PoPs), sit in hundreds of cities. Your own servers are the origin, the publisher. When a user asks for an image:
- Cache hit: the nearest edge already has it, and answers in a few milliseconds.
- Cache miss: the edge asks your origin, stores the response, and answers. The next user nearby gets a hit.
Requests reach the nearest PoP via anycast or DNS-based routing. Each PoP caches by cache key (by default scheme, host, path and query string) according to the origin’s Cache-Control (s-maxage for shared caches, max-age, private, no-store) and its own rules. Misses go to the origin, often through a mid-tier or origin shield that collapses misses from all PoPs into one origin request per object. Beyond static files, CDNs terminate TLS close to users (saving handshake round trips), absorb DDoS traffic, run edge functions, transform images, and can cache API responses that are public or keyed safely.
A request through a CDN
Tap a box to see what it does.
User in Sydney
DNS or anycast routes the request to the nearest edge, a few milliseconds away instead of 300 ms to Frankfurt.
Edge: Sydney
Looks up the cache key (host + path + selected query parameters). A hit answers immediately; a miss continues to the shield.
Origin shield
A second cache layer close to the origin. Misses from 300 edges become one request to the origin per object, which protects it during spikes and purges.
Origin
Your servers or object storage (S3, GCS). They set Cache-Control on every response, which tells the CDN what it may store and for how long.
# A product image with a content hash in its name: never changes, cache for a year
GET /img/lamp.3f9a2c1e.webp
Cache-Control: public, max-age=31536000, immutable
# The public product list: browsers 60 s, the CDN 5 minutes, serve stale while refreshing
GET /api/products?category=lamps
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60
# The user's own profile: may be cached by the user's device only, never by the CDN
GET /api/me
Cache-Control: private, max-age=0, must-revalidate
# A payment response: don't store anywhere
POST /api/payments
Cache-Control: no-storepublicmeans shared caches like CDNs may store it;privatemeans only the user’s own device may.max-ageis how many seconds a copy may be used;s-maxageoverrides it for shared caches (the CDN), so the CDN can keep things longer than phones do.immutablewith a longmax-ageis for files whose name changes when their content changes: they never need checking again.no-storemeans don’t keep a copy at all.
Tap the line with the bug.
@GetMapping("/api/me")
fun me(@AuthenticationPrincipal jwt: Jwt): ResponseEntity<ProfileDto> =
ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(5)).cachePublic())
.body(profiles.load(jwt.subject))@GetMapping("/api/me")
ResponseEntity<ProfileDto> me(@AuthenticationPrincipal Jwt jwt) {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(5)).cachePublic())
.body(profiles.load(jwt.getSubject()));
}app.get("/api/me", requireAuth, async (req, res) => {
res.set("Cache-Control", "public, max-age=300");
res.json(await profiles.load(req.user.id));
});Hint: The URL is the same for every user. Which part of the request identifies the user, and is it in the CDN's cache key?
The response is personal, but marked public, so the CDN stores it under the cache key /api/me, which is identical for every user. The Authorization header isn’t part of the key. The first user’s profile is now served to everyone who opens the app for the next five minutes: names, emails, addresses. This kind of misconfiguration has caused real data leaks at large companies.
Personal or authenticated responses are private (or no-store if even the device shouldn’t keep them):
.cacheControl(CacheControl.noCache().cachePrivate()).cacheControl(CacheControl.noCache().cachePrivate())res.set("Cache-Control", "private, no-cache");As a second line of defense, configure the CDN to bypass caching for requests with an Authorization header or session cookie unless a rule explicitly allows it, and set no-store as the default for /api/**.
- Anycast/DNS to the nearest PoP; cache by key per
Cache-Control(s-maxagefor shared caches,private,no-store); origin shield collapses misses. - Also: TLS termination near users, DDoS absorption, image transforms, edge functions.
- Authenticated or personal responses:
private/no-store; CDN rules bypass caching onAuthorization/session cookies by default.
Check this part
What happens on a CDN cache miss?
Which header keeps a personal response out of the CDN?
What does an origin shield do?