CDN Delivery
Build output to an edge near the user: what the cache key is made of, why invalidation is a worse tool than versioning, and why edge caching is mostly about latency rather than bandwidth.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What actually happens between my build output and a byte arriving at a user's device, and which part of it can I control?
A person on the other side of the world opens the application. They should not pay for the distance between them and wherever the deploy pipeline happened to run.
Put the assets on a CDN. It has servers everywhere, so the files arrive faster.
"Faster" is not one thing. Being closer removes round trips; it does not add bandwidth to the user's connection. A large file on a slow link is slow from the edge too.
- "Faster" is not one thing. Being closer removes round trips; it does not add bandwidth to the user's connection. A large file on a slow link is slow from the edge too.
- You put the assets on a CDN and the first user in each region still waits, because their request was a miss and the edge had to fetch from the origin anyway — with the extra hop.
- You deploy a change and half the world sees it. The other half sees the previous version for hours, because their edge still holds a fresh copy and nothing told it otherwise.
- You purge the cache to fix that, and every edge in the world simultaneously misses and stampedes your origin, which now serves the traffic it was put behind a CDN to avoid (Cache Stampede: Everyone Misses at Once in Performance).
- You add a cookie to the site, and your cache hit rate collapses, because the cache key now includes something that is different for every user.
What is actually happening
In the browser, not in the framework.
- A CDN is a fleet of caching reverse proxies placed close to users. A request goes to the nearest point of presence, which either has the response (a hit) or fetches it from the origin and stores it (a miss).
- Proximity mostly buys round trips, not bandwidth. Connection setup, TLS negotiation and every subsequent request-response exchange are all round-trip bound, and a round trip is bounded by distance (Latency Is a Distribution, Not a Number in Performance).
- Shorter round trips also mean a transport connection reaches full throughput sooner, because the congestion window grows per round trip. So proximity improves effective bandwidth indirectly, on top of the direct latency win (Congestion Control: Protecting the Network in Networking).
- The cache key decides what counts as the same response. By default it is roughly method, host and path; query strings,
Varyheaders, cookies and encoding may or may not be included, and every addition multiplies the number of entries and divides the hit rate. - Edges are independent. A purge is a distributed operation across a fleet, so it is eventually consistent by nature, and "purged" means "asked everywhere" rather than "gone everywhere".
- Which is why versioning beats invalidation: if a change produces a new URL, there is nothing to invalidate. The old entry expires quietly and is never requested again (Content-Hashed Assets).
- The edge can also cache things that are not assets — HTML, API responses — governed by
s-maxageandstale-while-revalidate, which apply to shared caches specifically (Browser HTTP Caching).
What this makes the browser do
And which of it is avoidable.
- The same work as any other origin: resolve, connect, negotiate, request. The CDN makes each of those steps shorter, not fewer.
- A separate connection per origin, so putting assets on a different hostname from the document costs a handshake the document's origin had already paid (Resource Hints).
- Storing whatever the edge said was cacheable in the browser's own cache, which is a second, independent layer with its own lifetime.
- Nothing else. The browser has no idea it is talking to a CDN, which is precisely the point and also why CDN misconfiguration presents to a frontend engineer as an inexplicable stale file.
Build output to a byte near the user
Four hops, each with its own cache and its own failure mode. Drawing it is worth the trouble because most CDN confusion comes from collapsing two of these boxes into one — usually the edge cache and the browser cache, which have different keys, different lifetimes and different owners.
The important asymmetry: you can purge the edge and you can never purge the browser. Whatever a browser stored with a long lifetime is out of your reach until it expires. That asymmetry is the entire argument for content-addressed names (Content-Hashed Assets).
The cache key is the contract
Hit rate is not a property of the CDN. It is a property of how many distinct keys your traffic produces, and every dimension you add to the key divides your hit rate by the cardinality of that dimension.
This is where most real CDN problems live, and they are almost never reported as caching problems. They are reported as "the site is slow in Australia" or "the origin fell over during the launch", and the cause is a query parameter or a cookie that nobody realised was part of the key.
| Key component | Usually included? | Why it matters | How it goes wrong |
|---|---|---|---|
| Path | Always | The identity of the resource | Rarely — except when the same content is reachable at several paths, multiplying entries |
| Query string | Usually, sometimes configurable | Distinguishes genuinely different responses | An analytics or campaign parameter appended to every link turns one object into thousands of one-hit entries |
| Host | Always | Multi-tenant edges must not mix sites | A site reachable on two hostnames caches everything twice |
Vary headers | As the response declares | The correct mechanism for genuinely different variants | Varying on something high-cardinality — a user agent string — is functionally the same as not caching |
Accept-Encoding | Normalised by most CDNs | Compressed and uncompressed are different bytes | Missing Vary behind a proxy that does not normalise serves the wrong encoding (Minification Is Not Compression) |
| Cookies | Provider-dependent | A cookie in the key is a per-user key | Adding a cookie to the site collapses the hit rate for every keyed path at once — the most common self-inflicted CDN outage |
Origin | Only if you set Vary: Origin | CORS responses differ per requesting origin | Without it, one origin's allow header is served to another and the request fails intermittently (CORS) |
Invalidate or rename
Every team eventually has to answer this, usually during an incident. The options differ mostly in how much of the answer depends on a distributed operation completing successfully across a fleet you do not operate.
The honest summary: invalidation is a recovery tool and versioning is a deployment strategy. A pipeline that relies on a purge for correctness is one failed purge away from serving two incompatible versions of an application, and it will not find out from an error — it will find out from users (Long-Lived Clients and Version Skew).
What makes an old cached copy stop being served?
when Always, for assets — scripts, styles, fonts, images referenced from code
cost A build that emits hashed names and a document that references them. In exchange: nothing to invalidate, a maximal edge and browser lifetime, and no possible stale-file incident (Content-Hashed Assets).
when A specific mutable URL was wrong and must stop being served now
cost A distributed operation with eventual-consistency semantics, plus a cache-miss burst for that path. Fine for one file, dangerous as a habit.
when A set of related responses changed together — a content release across many pages
cost Requires tagging responses at the origin, which is real work up front. Much safer than purging everything, and provider-specific.
when An incident, and you need certainty more than you need the origin to stay up
cost Every edge misses at once and the origin absorbs full global traffic. A legitimate emergency tool and an illegitimate deploy step.
when The response is genuinely dynamic — a document, an API response, anything personalised
cost A lower hit rate by design. Pair with stale-while-revalidate so the refresh happens in the background rather than in front of a user.
# build writes stable names dist/app.js dist/app.css # deploy overwrites them at the origin aws s3 sync dist/ s3://assets/ # and then hopes cdn purge --path "/app.js" --path "/app.css"
# build writes content-addressed names dist/app.a3f19c.js dist/app.7b20e4.css # upload the new files first: they collide with nothing aws s3 sync dist/ s3://assets/ --exclude "index.html" # then publish the document that names them, last aws s3 cp dist/index.html s3://assets/index.html \ --cache-control "no-cache" # nothing to purge; the old files expire unreferenced
The first version has a window during which the old document is still cached at some edges while the origin already serves new bytes under the old names — a user gets last release's HTML with this release's JavaScript, and the failure is a blank page rather than an error. The second has no such window: new assets are uploaded under names nothing references yet, and the document that starts referencing them is published last and is itself not cached long. Ordering, not purging, is what makes the deploy atomic from the browser's point of view.
How to build it
Most important first.
- Version rather than invalidate. Content-addressed asset names remove the entire class of problem, and are the reason a long edge lifetime is safe (Content-Hashed Assets).
- Keep the cache key as small as it can be. Strip query parameters that do not change the response, avoid
Varying on anything with high cardinality, and never let a per-user cookie into the key for static assets. - Separate the document's caching policy from the assets'. The document is the thing that names everything else, so it should be short-lived at the edge and revalidated in the browser.
- Serve the document from the same origin as the assets where you can. One origin, one connection, no extra handshake, and hints stop being necessary.
- Use
stale-while-revalidateat the edge to make the first user in a region not the one who pays for the miss. Serving slightly stale content while refreshing in the background is usually the right trade for a document. - Know which layer sets the headers your users receive. It is frequently the CDN rather than your application, and the two can disagree in ways that are invisible in development (Deploying a Frontend).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- No direct accessibility surface, and a substantial indirect one: proximity removes round trips, and round trips are exactly the interval during which a page has nothing for assistive technology to navigate.
- A stale edge serving an old document alongside new assets — or the reverse — produces partially broken pages, and the most common visible break is missing CSS, which destroys reading order cues and contrast (Contrast, Colour and Motion).
- Fonts served from the edge arrive sooner, which reduces the text swap that reflows content mid-sentence for magnifier and screen-reader users (Images and Fonts).
- If your CDN serves a maintenance or error page, it is a page too. It needs a heading, a landmark, a title and a focusable route out of it — error pages are the most reliably inaccessible documents on the internet.
What can go wrong
- A stale edge entry after a deploy, with no visibility into which regions are affected — the classic "works for me, and for everyone in my city".
- A purge-everything reflex on every deploy, which converts a routine release into an origin load spike and a global cache-miss storm.
- Cache key pollution: an analytics query parameter appended to every URL turns one cached object into thousands of one-hit entries.
- A
Set-Cookieon a static asset response, which many CDNs treat as a signal not to cache at all. The hit rate drops to nothing and nothing warns you. - CORS headers cached without
Vary: Origin, so one origin's allow header is served to a different origin and the request fails in a way that reproduces only sometimes (CORS). - Caching an authenticated response at the edge because
privatewas missing — the failure where a performance improvement becomes a data leak.
- A deploy and an in-flight request race: a user can receive a document from the new build and assets from the old edge entry, or the reverse.
- Many edges miss simultaneously after a purge and race to the origin for the same object, which is the stampede that collapsing or request coalescing exists to prevent.
- Two regions can serve two different versions of the same URL at the same moment, which is not a bug in the CDN — it is what "eventually consistent" means, applied to your deploy.
- The edge terminates TLS, which means the CDN sees plaintext requests and responses. That is a real trust relationship, not an implementation detail (TLS as a Security Boundary in Security Engineering).
- Anything cacheable at a shared cache can potentially be served to another user.
private,no-storeand a correctVaryare the controls, and all three are one-line mistakes. - Cache poisoning is a real class: if an unkeyed request header influences the response, an attacker can store a response of their choosing under a key other users will hit.
- A CDN is also a place to enforce things — security headers, a Content-Security-Policy, request filtering — which means it is a place where those things can be silently absent for one route (Content Security Policy).
- Serving your own copies of third-party assets from your CDN removes an external origin from the critical path and moves the integrity responsibility to you (Third-Party Scripts and the Supply Chain).
- "A CDN makes downloads faster." It makes them *closer*. Bandwidth is the user's; distance is what you bought.
- "We have a CDN, so caching is handled." A CDN with a cache key that includes a per-user cookie is an expensive proxy.
- "Purging is the way to deploy." Purging is the escape hatch. Renaming is the deploy (Content-Hashed Assets).
- "Edge caching only matters for static assets." Documents and API responses often benefit more, because they are the ones on the critical path — with correspondingly higher correctness stakes.
- "A CDN protects the origin." It reduces load for cacheable responses. Anything uncacheable passes straight through, which is why a purge storm hurts.
Measuring it, and what changes in the field
- The response headers say whether it was a hit, and most CDNs expose the edge location too. Check them from more than one region — from one location you are measuring one edge.
- The Network panel's waiting segment is where proximity shows up: an edge hit and an origin miss differ by the distance to the origin, visible directly (Reading a Network Waterfall).
- Hit rate by path in the CDN's own reporting, which is where cache-key pollution appears as a long tail of one-hit objects.
- Field data segmented by region. A global average hides exactly the population a CDN exists to serve (Real User Monitoring).
- On a distant, high-latency connection, an edge is worth the most — it is removing the multiplier on every round trip.
- On a slow but nearby connection it is worth much less: the bytes still have to fit through the same pipe.
- On a first-in-region request the edge is a *cost*, not a saving: a miss adds a hop.
stale-while-revalidateand pre-warming are how you stop a real user paying for it. - For a large single file, throughput dominates and the edge helps mainly through faster congestion-window growth rather than through the cache itself.
- A CDN adds an operational layer that can be misconfigured, and its configuration usually lives outside the frontend repository where frontend engineers cannot see it in a diff.
- Long edge lifetimes maximise hit rate and maximise the blast radius of a mistake. Content addressing is what makes that trade safe.
- A separate asset origin gives independent scaling and costs a connection setup; a single origin is simpler and couples asset traffic to document traffic.
- Edge-caching HTML is the largest available win and the largest available correctness risk, because HTML is the response most likely to be personalised (Egress: Moving Data Costs Money, Not Just Storing It in Cloud & Infrastructure).
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALEdge caching, cache keys and the miss-then-fill model are common to every CDN. What differs is vendor behaviour at the margins — which headers are keyed by default, whether a
Set-Cookiedisables caching, how purges propagate, and whether stale-serving is supported — so a configuration that is correct on one provider can silently do nothing on another. - NETWORK-SPECIFICThe benefit scales with round-trip time, so an edge is transformative on a distant cellular connection and marginal on a fast local one. It also interacts with protocol: HTTP/3 over QUIC removes a round trip from connection setup, which reduces exactly the cost proximity was buying back (HTTP/3 and QUIC in Networking).
- PLATFORM-SPECIFICWhere the CDN configuration lives — a provider console, a Terraform module, a framework adapter, or a file in your repository — determines whether a frontend engineer can even see the caching policy in a code review. This is an organisational fact with direct technical consequences.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Distributed Systems — a CDN fleet is a geo-replicated cache with no coordination between replicas, so a deploy is a convergence problem and a purge is a best-effort broadcast.