What a CDN actually is
A globally distributed set of reverse-proxy caches — PoPs (points of presence) — that sit between users and your origin. A request goes to the nearest PoP; if it has a fresh copy it answers immediately, otherwise it fetches from origin and keeps a copy.
Two mechanisms do the work:
Anycast routing. The same IP address is advertised from every PoP, and internet routing delivers the packet to the topologically nearest one. No DNS trickery, no client logic, and failover is fast — if a PoP withdraws its route, traffic moves automatically.
Tiered caching / origin shield. PoPs don't each fetch from your origin independently. They fetch from a regional parent cache, which fetches from origin. Without this, a popular uncached object triggers a simultaneous fetch from every PoP — a thundering herd proportional to the CDN's size. The shield collapses that into one request.
The cache key is the whole game
A CDN caches a response against a key. By default the key is the URL, but everything that gets added to it multiplies your object count and divides your hit rate.
Things that commonly and accidentally wreck the key:
- Cookies. Many CDNs treat any
Set-Cookiein a response as "this is personalised, don't cache." One analytics cookie applied globally makes the whole site uncacheable — the hook above. - Query strings.
?utm_source=…creates a distinct cache entry per campaign for what is byte-identical content. Strip or allowlist query parameters. Varyheaders.Vary: Accept-Encodingis fine and necessary.Vary: User-Agentfragments your cache across thousands of UA strings and is almost always a mistake.- Authorization headers, which correctly suppress shared caching — but only need to apply to genuinely private routes, not to the whole domain.
The corresponding discipline: serve static assets from a path or host with no cookies at
all, normalise query strings, and be deliberate about Vary.
Cache-Control, precisely
These directives get misused constantly:
| Directive | Meaning |
|---|---|
max-age=N |
Fresh for N seconds, for any cache |
s-maxage=N |
Same, but for shared caches only — overrides max-age at the CDN |
public |
Cacheable by shared caches even if normally it wouldn't be |
private |
Browser may cache; CDN must not |
no-cache |
May store, but must revalidate before use — not "don't cache" |
no-store |
Must not store at all. This is the one people mean when they write no-cache |
immutable |
Never revalidate within the freshness window |
stale-while-revalidate=N |
Serve stale for up to N seconds while refreshing in background |
stale-if-error=N |
Serve stale for up to N seconds if the origin is failing |
The pairing that solves most problems: s-maxage long at the CDN, max-age short in the
browser. You can purge the CDN; you cannot purge a browser. Keep the copy you control
long-lived and the copy you don't short-lived.
stale-if-error deserves special mention: it turns an origin outage into stale-but-working
content instead of an error page. It is close to free resilience and routinely unused.
Invalidation
Versioned URLs (fingerprinting) — app.a4f9c2.js. The content hash is in the filename,
so a change produces a new URL that was never cached. Combine with max-age=31536000, immutable. This is the correct approach for build assets, and it needs no purge at all.
Purge / invalidate — tell the CDN to drop an object. Necessary for content at stable URLs (an article, a product page). Purges take seconds to propagate globally and are typically rate-limited, so don't design a system that purges thousands of objects per minute.
Surrogate keys / cache tags — tag responses (product-1234, category-shoes) and purge
by tag. One product update invalidates every page that displays it. This is the feature that
makes CDN-caching dynamic HTML genuinely practical, and it's the thing to reach for when an
interviewer asks how you'd cache a content site.
Caching dynamic content
Static assets are the easy half. The gains are larger on dynamic content, and so is the risk.
- Short TTLs on hot pages. A homepage cached for 10 seconds absorbs almost all of a traffic spike, and 10 seconds of staleness is usually invisible.
- Split the page. Cache the shared shell aggressively, fetch the personalised fragment client-side. The 5% that's per-user shouldn't make the other 95% uncacheable.
- Edge compute. Workers/Lambda@Edge run code at the PoP: A/B assignment, auth checks, header rewriting, geo-routing, request coalescing. Latency is PoP-local rather than origin-local.
- Cache by user segment, not by user.
Varyon a cookie with three values (anonymous, logged-in, premium) keeps a usable hit rate; varying on user ID does not.
When not to use one
- Genuinely per-user, per-request data. Nothing to share.
- Write-heavy APIs. CDNs cache reads; writes pass through and gain nothing.
- Small single-region audiences. If every user is within 20 ms already, the win is marginal — though origin offload and DDoS absorption may still justify it.
- Strict data residency. Caching EU personal data in PoPs worldwide may be a compliance problem before it's an engineering one. Most CDNs support restricting PoPs by region, and for Swiss/EU work this is a question worth asking early.
What to take away
- A CDN is a distributed reverse proxy; anycast finds the nearest PoP and origin shielding stops every PoP stampeding your origin.
- Hit rate lives or dies by the cache key. Cookies, query strings, and
Varyare the usual culprits. no-cachemeans revalidate;no-storemeans don't store. Uses-maxagelong,max-ageshort.- Fingerprint build assets so they never need purging; use surrogate keys for content.
stale-if-errorconverts origin outages into stale content for almost no effort.- Caching a personalised response under a shared key is the failure mode that ends careers.
Check yourself
-
Your CDN hit rate is 4%. What is the most likely cause?
A low hit rate almost always means responses are not being considered cacheable or are fragmented across too many keys. A blanket Set-Cookie, unnormalised UTM parameters, or Vary: User-Agent each do this. PoP distance affects latency, not hit rate.
-
What does Cache-Control: no-cache actually instruct a cache to do?
no-cache permits storage but requires revalidation before each use. The directive meaning 'do not store this at all' is no-store. Confusing the two is one of the most common HTTP caching mistakes.
-
Why does a CDN use tiered caching or an origin shield?
Without a shield, an uncached popular object produces one origin fetch per PoP — a thundering herd scaled to the size of the CDN. A regional parent cache collapses those into a single origin request.
-
You need build assets to update instantly on deploy without ever issuing a purge. What is the standard approach?
Fingerprinting means changed content gets a new URL that was never cached, so there is nothing to invalidate. That lets you cache aggressively with immutable. Surrogate keys are the right tool for content at stable URLs, where the URL cannot change.