Question presented to candidate:
"We serve images, JavaScript and video to users all over the world from one region, and pages are slow far from it. What is a CDN, how does a request actually get served by one, and what do we have to get right so it helps rather than serving stale content?"
What a strong answer should cover:
What it is: a network of edge servers in many PoPs, acting as a caching reverse proxy in front of your origin; users are steered to a nearby edge by anycast or DNS
The request path: edge cache hit served locally; cache miss fetched from the origin (often through a tiered cache / origin shield), stored, then served
Control: Cache-Control (max-age, s-maxage, stale-while-revalidate), the cache key, and invalidation by TTL, purge, or versioned file names
What drives cache hit ratio and origin offload: popularity skew, cache size, number of PoPs, and how much traffic is cacheable at all
Beyond static files: request collapsing, TLS termination near the user, DDoS absorption, and edge compute; what not to cache (personalised responses)
Code / implementation expected: No β the request flow, headers and trade-offs are the answer; the example simulates hit ratios and the effect of a shield.
cdn
2
Try it yourself
Sketch your approach before you read the solution. That is what the interview tests.
Stuck? Get a hint.
A short nudge toward the approach. It will not give the answer away.
3
Solution
Target Audience: Beginners to system design β you know what a web server is; no networking background assumed.
Difficulty: Medium
Verified with: Node 22.22 and Python 3.12 (both print identical output).
How to read this doc: Every idea is explained in plain language first, then tagged with π Interview term: so you also learn the exact words an interviewer expects. Hit ratios and fetch counts come from running the example; the Verified section quotes the full output.
1. Why This Even Matters β A Story First
A bakery in Paris makes the best croissants in the world, and people in Tokyo want them. Flying each croissant from Paris on order takes hours. So the bakery opens small shops in Tokyo, New York and SΓ£o Paulo. Each shop keeps the most popular items on its shelves, restocked from Paris. A customer in Tokyo walks into the Tokyo shop; most of the time the cr
The solution is hidden so you can try first
13 min read Β· 12 sections
4
Read the code
CDN edge caches: hit ratio, origin shield, request collapsing, TTL vs purge
// A CDN in miniature: edge PoPs with LRU caches, an optional origin shield,// request collapsing, and what a TTL means for freshness after an update.// Objects follow a Zipf popularity curve; randomness is a fixed-seed LCG.let seed = 42;
functionrand() { // 32-bit LCG, identical in every language
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return seed / 4294967296;
}
constOBJECTS = 20000;
const cdf = [];
let sum = 0;
for (let k = 1; k <= OBJECTS; k++) { sum += 1 / k; cdf.push(sum); }
functionpick() { // Zipf(s=1): object k is requested with weight 1/kconst u = rand() * sum;
let lo = 0, hi = OBJECTS - 1;
while (lo < hi) { const mid = (lo + hi) >> 1; if (cdf[mid] < u) lo = mid + 1; else hi = mid; }
return lo;
}
classLRU {
constructor(cap) { this.cap = cap; this.map = newMap(); }
get(k) {
if (!this.map.has(k)) returnfalse;
this.map.delete(k); this.map.set(k, true); returntrue;
}
put(k) {
if (this.map.has(k)) this.map.delete(k);
elseif (this.map.size >= this.cap) this.map.delete(this.map.keys().next().value);
this.map.set(k, true);
}
}
functionsimulate(edgeCap, pops, shieldCap) {
seed = 42;
const edges = Array.from({ length: pops }, () =>newLRU(edgeCap));
const shield = shieldCap ? newLRU(shieldCap) : null;
let edgeHits = 0, origin = 0;
const N = 200000;
for (let i = 0; i < N; i++) {
const obj = pick();
const edge = edges[i % pops]; // users in different regions reach different PoPsif (edge.get(obj)) { edgeHits++; continue; }
if (shield && shield.get(obj)) { edge.put(obj); continue; }
origin++;
if (shield) shield.put(obj);
edge.put(obj);
}
return { edgeHit: (100 * edgeHits) / N, origin, offload: (100 * (N - origin)) / N };
}
console.log("Part 1: 200000 requests, 20000 objects (Zipf), spread over PoPs");
console.log(" pops edge cache shield edge hit origin fetches offload");
for (const [pops, cap, sh] of [[1, 1000, 0], [1, 4000, 0], [8, 1000, 0], [8, 4000, 0], [8, 1000, 8000], [8, 4000, 8000]]) {
const r = simulate(cap, pops, sh);
console.log(` ${String(pops).padStart(4)}${String(cap).padStart(10)}${String(sh || "-").padStart(6)}${r.edgeHit.toFixed(1).padStart(8)}% ${String(r.origin).padStart(15)}${r.offload.toFixed(1).padStart(7)}%`);
}
console.log("\nPart 2: a new video goes live, 500 requests reach one PoP over 1000 ms");
const fetchMs = 200; // the origin takes 200 ms to answer the first misslet naive = 0, collapsed = 0, waited = 0, hits = 0;
for (let i = 0; i < 500; i++) {
const t = Math.floor((i * 1000) / 500); // arrival time in msif (t >= fetchMs) { hits++; continue; } // the object is cached once the first fetch returns
naive++; // without collapsing, every request during the fetch is another missif (i === 0) collapsed++; else waited++; // with collapsing they wait for the one fetch
}
console.log(` arrivals during the first fetch: ${naive}, arrivals after it: ${hits}`);
console.log(` without request collapsing: ${naive} origin fetches`);
console.log(` with request collapsing: ${collapsed} origin fetch, ${waited} requests wait for it`);
console.log("\nPart 3: object changes at t=100s, cached with max-age=300 at t=0s");
const timeline = [50, 150, 299, 301];
for (const t of timeline) {
const ttlServe = t < 300 ? "old (cached copy still fresh)" : "new (expired, refetched)";
const purgeServe = t < 100 ? "old" : "new (purged at t=100s)";
console.log(` t=${String(t).padStart(3)}s TTL only: ${ttlServe.padEnd(30)} TTL + purge: ${purgeServe}`);
}
5
Discussion
Other approaches, follow-ups and how it went in real interviews.