CDN and edge caching
covers POPs and cache hierarchy · cache keys and fragmentation · purge vs versioned URLs · origin shielding · signed URLs and cookies · edge compute
What a CDN actually caches
A CDN is a fleet of caching servers in data centers close to users, called points of presence (POPs). A request goes to the nearest POP first. If that POP has the response stored and fresh, it answers from there (a HIT) and origin never hears about it. If not (a MISS), the POP fetches from origin, stores a copy, and serves it. Everything about CDN performance is a fight to turn MISSes into HITs.
What gets cached is narrower than people assume. Cloudflare, for example, caches by file extension and does not cache HTML or JSON by default: images, CSS, JS, fonts, video, and archives are on the default list, dynamic documents are not. A response also drops out of caching if it carries Set-Cookie, if the method is not GET, or if Cache-Control says private, no-store, or max-age=0. This is the split between static acceleration (cache the file, serve it from the edge) and dynamic acceleration (the content is per-request, so the edge cannot store it and instead speeds up the trip to origin over optimized network paths). The freshness rules themselves (Cache-Control, max-age, s-maxage, ETag revalidation) are HTTP caching semantics covered in the http-caching module; here the question is what the edge does with them.
The cache key, and why query strings wreck hit rate
When a POP looks for a stored response, it looks it up by a cache key. By default the key is the scheme, the host, and the URI including its full query string. Two requests share a cached copy only if their keys match exactly.
That last part bites. /post/42 and /post/42?utm_source=news are different keys, so identical HTML gets stored twice. Point marketing at your pages with ?utm_source, ?ref, ?fbclid tacked on, and one article fragments into hundreds of separate entries, each cold on its first hit. The hit ratio collapses even though the bytes are the same.
The fix is cache-key normalization: tell the CDN to ignore (or allowlist) the parameters that do not change the response, so all the tracking variants collapse to one entry.
# default key: full query string is significant
/post/42?utm_source=news -> separate cache entry (MISS)
/post/42?utm_source=email -> separate cache entry (MISS)
# after normalization: strip utm_* / ref before the lookup
/post/42 -> one shared entry (HIT after the first)
Wrong "The query string does not matter, CDNs key on the path." The default key includes the whole query string. Anything you append forks the cache until you normalize the key. Request headers pulled in via Vary and any cookies you key on fragment it the same way.
Invalidating: purge the old copy or change the URL
You have two ways to make sure users stop seeing a stale copy.
Purge (also called invalidation) tells the CDN to drop a cached entry so the next request re-fetches from origin. Every CDN offers purge by single URL and purge everything; more surgical options (by tag, by hostname, by URL prefix) exist too. Purge everything is the blunt instrument: it empties the entire cache, so the next request for every URL is a cold MISS all at once, which stampedes origin. Reach for single-URL or grouped purges instead.
Versioned URLs are the other approach: put a content hash or version in the filename (app.3f9c2b.js) so a change ships as a new URL. The old URL is never edited, so there is nothing to purge, and you can cache it for a year. This is why build tools fingerprint asset filenames.
Wrong "Purge everything on every deploy is the safe default." It guarantees a cold cache and a synchronized origin spike right after each deploy. Purge only what changed, or version the URL so no purge is needed.
Private content: signed URLs
Not everything at the edge should be public. For paid downloads, member videos, or private media, you do not want a guessable URL that anyone can share. A signed URL carries an expiring, cryptographically signed token in the query string; the CDN validates the signature and expiry before serving and rejects anything tampered with or expired. Signed cookies do the same job for a whole set of files at once. The senior notes cover when to pick which.
Cache hit ratio is the number that matters
The one metric that summarizes a CDN's job is cache hit ratio: the share of requests served from the edge instead of origin. A higher ratio means less origin load, lower latency, and lower egress cost, so it is the number you watch when you tune keys, TTLs, and purge behavior.
You read it two ways. Per response, the CDN stamps a cache-status header (cf-cache-status on Cloudflare, x-cache on CloudFront) with values like HIT, MISS, EXPIRED, or BYPASS, so you can see why any single request did or did not cache. In aggregate, the CDN's analytics report the ratio over time. A useful analytics detail: a good hit-ratio number excludes traffic that was never cacheable in the first place, so it measures how well the cacheable content is actually caching rather than being dragged down by dynamic requests that could never hit.
Origin shielding and the cache-miss stampede
A flat CDN has a hidden failure mode: every POP is an independent origin client. When a popular object expires or gets purged, dozens of POPs MISS it within the same second and each opens its own fetch to origin. Origin sees a synchronized burst of identical requests, a thundering herd on one key, exactly when the object is hottest.
Tiered cache fixes this by putting a hierarchy between the edge and origin. Lower-tier POPs (nearest the user) never talk to origin directly; on a MISS they pull from an upper-tier POP, and only the upper tier contacts origin. Concentrating the misses through one upper tier does two things: it raises the hit ratio (the upper tier has seen the object for some other region already), and it lets the CDN collapse concurrent requests for the same object into as few as one origin fetch. CloudFront calls the dedicated version Origin Shield; Cloudflare calls the topology Tiered Cache, with a Smart option that picks a single upper tier per origin automatically.
Wrong "Adding more POPs protects origin." More POPs means more independent origin requesters and a bigger herd. What protects origin is fewer things allowed to talk to it: a shield tier plus request collapsing. CloudFront also notes shielding pays off for a multi-CDN setup (point the other CDNs at CloudFront so origin sees one requester) and for just-in-time work like live-stream packaging or on-the-fly image processing, where a duplicate origin request is expensive. It is a poor fit for low-cacheability dynamic content, which passes straight through the shield as an extra billed hop.
Invalidation that does not cold-start the cache
Two levers separate a senior invalidation design from "purge everything."
The first is grouping. Tag responses at origin with a cache tag (Cloudflare's Cache-Tag header) or a surrogate key (Fastly's Surrogate-Key header), listing every logical group an object belongs to: article:42, section:tech, homepage. One object can carry many keys. When an article changes you purge the key article:42, and the CDN invalidates every URL tagged with it (the article page, the section listing, the API response) in one call, targeting content groups rather than enumerating URLs. This is how you invalidate "everything touched by this edit" without touching anything else.
The second is soft purge. A hard purge makes the object immediately unusable, so the next request is a guaranteed MISS to origin. A soft purge instead marks the object stale: combined with stale-while-revalidate, a request right after the purge gets the stale copy served instantly while the edge revalidates in the background, so users never wait on origin and origin never sees a synchronized MISS. On Fastly you turn a purge soft with Fastly-Soft-Purge: 1; purge-all cannot be soft. Pair it with stale-if-error and the edge keeps serving the last good copy through an origin outage.
# hard purge: next request is a forced MISS to origin
purge /post/42
# soft purge + stale-while-revalidate: serve stale, revalidate in background
Fastly-Soft-Purge: 1 -> object marked stale, not evicted
next request: stale served now, refresh async
Where content can carry a new identity, versioned URLs beat purging outright: an immutable hashed URL is never invalidated, so there is no purge to propagate and no stampede to absorb.
Signed URLs versus signed cookies
Both restrict private content and both carry an expiring signature the edge validates before serving; they differ in scope. AWS's guidance is the clean split. Use a signed URL when you are gating one file (an app installer, a single document) or the client is a custom HTTP client that will not carry cookies. Use signed cookies when you are gating many files behind one grant (all the segments of an HLS video, a whole subscriber area) or you cannot change the URLs your app already serves.
The trap is combining them carelessly. On CloudFront a signed URL takes precedence over a signed cookie for the same file, and any URL that already contains Expires, Signature, Key-Pair-Id, or Policy is treated as a signed URL, so the cookie is ignored. Sign with a key group whose private key stays server-side; the edge holds only the public key to verify.
Wrong "A signed URL is cached per user, so it hurts my hit ratio." The signature lives in the query string, and you configure the cache key to ignore the signature parameters, so every valid viewer of the same object shares one cached entry. The edge validates the signature on the request before it serves the shared object; validation and cache lookup are separate steps.
What belongs at the edge
Edge compute runs your code in the POP instead of at origin. The platforms split into two weight classes, and putting logic in the wrong one is a common design mistake.
The lightweight class here is CloudFront Functions: JavaScript, sub-millisecond, with tight CPU and memory budgets (around a couple of MB) and no filesystem, no request body, and no network access. It runs on the viewer request/response, before and after the cache. That placement is the point: work that shapes the cache key or the request belongs here because it executes before the lookup. Canonical uses are cache-key normalization (strip utm params to lift the hit ratio), header rewrites, redirects, and validating a hashed token like a JWT by inspecting a header. Cloudflare Workers also runs on the viewer path but is a fuller runtime, with fetch() network access and request-body access and higher CPU and memory limits, closer to the heavyweight class than to CloudFront Functions.
The heavyweight class (Lambda@Edge) runs Node or Python with real CPU, memory, network, and filesystem, and can hook the origin request/response. It is where you put anything that calls an external service, reads the request body, pulls a third-party library, or runs longer than a millisecond. Origin-facing triggers run at the regional edge cache (the shield region), not in every POP, so they execute far less often than viewer-facing ones.
Wrong "Code at the edge is closer to the user, so moving page rendering there is always faster." Edge functions carry hard CPU and time limits; heavy templating there loses to serving a pre-rendered page as a cache HIT, which does zero compute per request. The other failure is subtler: rewriting a response per user in a viewer-response function and letting it cache under the shared key serves one user's page to the next. Personalization has to stay out of the shared-cache path, whether that is a client-side fetch, an edge-composed fragment, or a private cache.
Measuring, and what the number hides
Cache hit ratio is the metric because it maps one-to-one to origin offload, but read it with care. A single response tells you its fate in the cache-status header (cf-cache-status, x-cache) with HIT, MISS, EXPIRED, STALE, REVALIDATED, DYNAMIC, or BYPASS, so a low aggregate ratio is a debugging exercise: sample the header and find which URLs report MISS, DYNAMIC, or BYPASS and why. DYNAMIC and BYPASS are different diagnoses. DYNAMIC means the asset was never eligible to cache in the first place (default-uncacheable HTML or JSON with no cache directives), which is what your uncached page HTML reports by default. BYPASS means the origin told the edge to skip an otherwise-cacheable response, via a no-cache, private, or max-age=0 directive or a Set-Cookie on the response. Default HTML reporting as DYNAMIC can never contribute a HIT, so a ratio dragged down by that is a caching-policy bug rather than a tuning problem. Good analytics exclude never-cacheable traffic from the denominator, which is why the dashboard ratio and a naive HIT/total from raw logs disagree. Track the ratio per content type, not just site-wide, or a well-cached asset tier will mask a static-HTML tier that is quietly all MISSes.
dig deeper
Primary sources behind these notes - the specs and official docs worth reading in full.
gapmap pro
You've read the CDN and edge caching notes. An interviewer will ask you to prove them.
Know you're ready. Don't hope.
The notes are free: every topic, senior depth. Pro is what turns reading into readiness you can prove:
Mock interviews on your topics
An AI interviewer that pushes back with follow-ups and grades you honestly. Per module, fair-use unlimited.
Voice test interviews
The dress rehearsal, out loud: questions from your map, a transcript, and a per-answer verdict.
A plan to your date
Ready or not ready, per module, as the interview approaches. Recomposed whenever your goal changes.
A mentor inside every lesson
Stuck on the senior notes? Ask, drill deeper, get re-quizzed on the spot.
Pro $20/mo · Pro+ $50/mo · every study note on the site stays free