49. Walkthrough: Design a CDN
The prompt. "Design a content delivery network." Variants: "Design CloudFront / Azure Front Door / Akamai", "Design a global caching layer for our static and video content", "How does a CDN purge content worldwide in seconds?" It's an infrastructure problem about routing, caching hierarchy, global config/purge distribution, TLS at scale, and multi-tenancy. The CDN guide in this wiki has a runnable topology for most of these mechanisms. This walkthrough is about presenting the design in an interview.
Step 0 — Clarify the business goal (≈ 3 min)
🎤 Candidate: Are we building a commercial multi-tenant CDN we sell to customers, like CloudFront or Front Door, or an internal CDN for our own content, like Netflix Open Connect? The first needs multi-tenancy, customer config, billing, and isolation. The second can be tuned to one workload.
🧑💼 Interviewer: Commercial and multi-tenant.
🎤 Candidate: Then our customers' outcomes are our product: latency (their conversion), origin offload / hit ratio (their infrastructure cost), availability, and security (DDoS, WAF). Our own business outcome is margin per GB delivered, which depends on bandwidth cost (peering vs transit) and hardware efficiency. Is video in scope?
🧑💼 Interviewer: Web assets and API acceleration mainly, with large-file and video support. Edge compute is out of scope.
Step 1 — Functional requirements
| In scope | Out of scope |
|---|---|
| Customer onboarding: hostnames, origins, cache rules (control API + console) | Edge compute / serverless functions |
| Serve HTTP/1.1, HTTP/2, HTTP/3 over TLS on customer domains | DNS hosting as a product |
| Cache static content; accelerate dynamic content (connection reuse to origin) | Video packaging/transcoding |
| Purge by URL, by tag (surrogate key), by prefix, everything | |
| Automatic TLS certificates for customer domains | |
| DDoS mitigation, basic WAF, per-customer rate limits | |
| Real-time analytics + logs + billing per customer |
Step 2 — Non-functional requirements
| NFR | Target |
|---|---|
| Footprint | ~300 PoPs; peak capacity 100 Tbps egress |
| Latency | Cache hit TTFB < 50 ms p95 for 95% of the world's users |
| Availability | 99.99%+ per customer; no single PoP failure visible beyond seconds |
| Purge | Global purge completes in < 5 s p99 (aim for ~1 s) |
| Config change | < 30 s globally (security rules faster), staged |
| Hit ratio | > 90–95% for cacheable traffic (customer's origin offload) |
| Isolation | One customer's attack or traffic spike can't degrade others |
| Billing accuracy | Bytes delivered per customer accurate to < 0.1% |
Step 3 — Estimates
Peak egress 100 Tbps = 12.5 TB/s; avg object (blend of web + large files) ≈ 100 KB
→ ~125M requests/s peak globally; largest PoPs ~3-5M req/s
Edge server: ~40-100 Gbps sustained with TLS on modern NICs/CPUs
→ a 5 Tbps PoP needs ~80-120 servers (+ N+1 headroom)
Cache storage: hot working set per large PoP: hundreds of TB (NVMe) + RAM for hottest objects
Logs: 125M req/s × ~500 B/log line ≈ 60 GB/s ≈ 5 PB/day raw
→ cannot ship everything: aggregate at the edge (per-customer counters/minute), sample raw logs,
ship full logs only for customers who pay for them (compressed, batched)
Billing: per-customer byte counters at each server, aggregated per minute → exact & cheap
Certificates: ~5M customer hostnames → millions of certs at every PoP (lazy-load by SNI)
Config: ~1M customer properties × ~10 KB ≈ 10 GB → must be replicated to every PoP
🎤 Candidate: So: routing and caching determine latency and hit ratio; config, purge, and certificate distribution to 300 PoPs is a big distributed-systems problem on its own; and logs must be aggregated at the edge, never shipped raw.
Step 4 — APIs & data model
Control plane (customers):
POST /v1/properties {hostnames[], origins[{host, weight}], rules[{match, ttl, cache_key, headers}]}
PUT /v1/properties/{id}/versions/{v}/activate (staging → production, versioned)
POST /v1/purge {urls[] | tags[] | prefixes[] | all:true} → {purge_id} GET /v1/purge/{id} → progress
GET /v1/analytics?property=…&metric=hit_ratio&interval=1m
Data plane: plain HTTP(S); origin returns Cache-Control, Surrogate-Key: "product-42 category-7"
| Data | Where it lives | Notes |
|---|---|---|
| Property config (versioned) | Global source of truth (strongly consistent DB) → replicated KV at every edge server | Read on every request, so it must be local and memory-mapped |
| Cert + key metadata | Cert store (encrypted) → edge on demand by SNI, cached | Private keys in HSM/KMS-protected store; optional keyless mode |
| Purge log | Append-only, sequence-numbered, replicated to PoPs | Edges track the last applied sequence |
| Cache entries | Per-PoP storage (RAM + NVMe) | Key = hash(normalized cache key) |
| Usage counters | Edge → regional aggregators → billing DB | Per customer per minute |
Step 5 — High-level architecture
┌──────────── GLOBAL CONTROL PLANE ─────────────┐
Customer API/console ────────▶│ Config svc (validate, version, staged rollout) │
│ Purge svc (sequence, fan-out, track acks) │
│ Cert svc (ACME issuance + renewal, §56) │
│ Analytics/billing pipeline │
└─────────┬───────────────────────────┬──────────┘
distribution tree │ (config, purge, certs) │ usage, sampled logs
┌──────────────────────────────┼───────────────┐ │
▼ ▼ ▼ │
┌─────────────── PoP (one of ~300) ──────────────────────────────────────────────────┐
│ Routers (BGP anycast) ─▶ L4 LB (ECMP + consistent hash, XDP DDoS drop) │
│ ─▶ Edge servers: TLS (SNI → cert), HTTP/1/2/3, WAF, rate limit, config lookup │
│ cache key → "owner" server in this PoP (consistent hashing) → RAM/NVMe │
│ miss ─▶ request collapsing ─▶ upper tier │
│ local config KV · purge log applier · counters │
└───────────────┬────────────────────────────────────────────────────────────────────┘
│ miss
▼
Regional tier / origin shield (few, large caches; one per origin region)
│ miss (collapsed; warm, pooled, long-lived connections)
▼
Customer origin
Walk a request: user resolves cdn.customer.com → CNAME to our anycast
address → BGP delivers it to the nearest PoP → L4 LB hashes the connection to
an edge server → TLS handshake with the customer's certificate selected by SNI →
property config lookup by host → WAF and rate limits → cache key computed per
rules → lookup on the owner server within the PoP → hit: respond (≈ 10–30 ms
TTFB) → miss: request collapsing (one fetch per key per PoP) → shield → origin
→ store with TTL → respond → usage counter incremented.
Step 6 — Deep dives
Deep dive 1: routing users to the right PoP
| Approach | How | Pros | Cons |
|---|---|---|---|
| Anycast | Same IPs announced via BGP from every PoP; the internet routes to the "closest" (in BGP terms) | Instant failover (withdraw the route), natural DDoS absorption across PoPs | BGP "closest" ≠ lowest latency; coarse control; route changes can break long TCP flows |
| DNS-based (GeoDNS) | Authoritative DNS returns the best PoP's IP per resolver location (EDNS Client Subnet helps) | Fine-grained, load-aware steering | DNS TTL caching slows failover; resolver location ≠ user location |
| Hybrid | DNS chooses among anycast "regions"/address pools; anycast within them | Control + resilience | Complexity |
🎤 Candidate: I'd use anycast for resilience and DDoS absorption, and manage load by withdrawing or prepending announcements from overloaded PoPs and by using different anycast prefixes per region, with DNS steering between prefixes. Real-user measurements continuously check whether the routing actually delivers low latency.
Deep dive 2: caching inside and across PoPs
- Within a PoP: if every server caches everything, a 100-server PoP stores each object up to 100 times and effective capacity shrinks 100×. So hash the cache key to an owner server (consistent hashing, §9), and let the server that received the request fetch from the owner over the PoP's fast network. Very hot objects are replicated to every server (detected by request rate) to avoid hot spots.
- Admission policy: a large share of objects are requested only once ("one-hit wonders"). Caching them evicts useful objects. Akamai described cache-on-second-hit using Bloom filters. Modern alternatives are TinyLFU-style admission or S3-FIFO eviction.
- Tiered caching: edge → regional/shield → origin. Hit ratios compound: a 90% edge and 80% shield hit ratio gives 98% offload (1 − 0.1 × 0.2). See CDN guide §5 for measured numbers.
- Request collapsing at both tiers: 10,000 concurrent misses for the same object become one origin fetch (the §7.3 stampede fix applied at the edge).
- Serve stale:
stale-while-revalidate(fast responses during refresh) andstale-if-error(origin down → serve the cached copy). This is often the biggest availability feature customers get. - Large files and video: cache by range slices (e.g. 1 MB chunks), so a seek doesn't need the whole file, and the first byte doesn't wait for a multi-GB origin fetch.
Deep dive 3: global purge in about a second
Customer POST /purge {tags:["product-42"]} ─▶ Purge svc: assign seq=8,812,331, persist
─▶ fan-out tree: control plane → ~10 regional relays → ~300 PoPs → all servers (persistent streams)
─▶ each server: apply purge → ack(seq)
─▶ purge status = "complete" when all PoPs ack (offline PoPs tracked separately)
HOW a tag purge is applied cheaply — generation numbers (no scanning):
tag_gen["product-42"] = 17 → 18 (O(1) update)
each cache entry stores the generations of its tags at fill time {product-42: 17}
on lookup: entry.gen(product-42)=17 < current 18 → treat as MISS (lazy invalidation)
- Lazy invalidation by generation turns "purge 2 million objects tagged X" into a single counter increment per server.
- Ordering: purges carry sequence numbers, and edges apply them in order.
- The offline-PoP problem: a PoP disconnected during a purge must not serve content on reconnect until it has caught up to the latest purge sequence (or it drops the affected portion of its cache). Otherwise it serves legally removed content.
- Purge vs in-flight fill race: a fill that started before the purge completes afterwards and stores stale content. Fix: record the generation or purge sequence at fill start, and discard the result if a purge happened meanwhile.
Deep dive 4: TLS for millions of customer domains
- Issuance: automate it with ACME (domain validation via HTTP-01 or DNS-01 through CNAME delegation) and renew at about 2/3 of lifetime. The certificate service is the subject of §56.
- Selection: SNI → certificate lookup in a local index. Load lazily, because millions of certs won't fit in every server's memory; keep an LRU cache of parsed certificates.
- Private key protection: keys encrypted at rest and decrypted only in memory on edge servers. For customers who won't share keys (banks), keyless TLS: the edge performs the handshake but sends the private-key signing operation to the customer's key server.
- Handshake cost: TLS session resumption, with ticket keys rotated frequently and shared within a PoP, plus ECDSA certificates (cheaper to sign than RSA).
Deep dive 5: multi-tenancy and DDoS
- Volumetric attacks: anycast spreads the attack across all PoPs; drop packets early in XDP/eBPF on the L4 layer; SYN cookies.
- Application-layer attacks: per-customer rate limits and bot scoring at the edge.
- Cache fairness: one customer's huge catalog can evict everyone else's content. Use per-customer or per-tier cache quotas (partitioned LRU), with priority for paying tiers. Isolation here is a pricing feature.
- CPU fairness: expensive WAF rules for one customer are capped by per-customer CPU budgets.
Step 7 — Failure modes, scale, cost, evolution
| Failure | Response |
|---|---|
| PoP overload or hardware failure | Withdraw anycast routes (traffic shifts to the next PoPs within seconds); keep headroom so neighbors absorb it |
| Origin down | Serve stale (stale-if-error); shield prevents a retry flood when the origin returns |
| Bad global config push | Staged rollout by PoP ring, automated health gates, instant rollback; config validated by actually loading it in a canary before broadcasting |
| Purge system lagging | Alert on purge latency SLO; edges serve with a bounded staleness guarantee; customers see purge progress |
| Certificate expiry | Renew early (2/3 of lifetime), alert on any cert within 14 days of expiry, keep a fallback issuer |
| BGP route leak/hijack | RPKI route-origin validation, monitoring of announcements |
Cost and business: bandwidth is the main cost. Settlement-free peering at internet exchanges and caches embedded in ISP networks cut transit costs dramatically. Hit ratio drives both customer value and our own transit bill. Hardware efficiency comes from Gbps per server and per watt. Unit metric: cost per delivered TB against price per TB, by region (some regions, such as parts of APAC and South America, have much higher transit prices).
Step 8 — Wrap-up
🎤 Candidate: We route with anycast for resilience and DDoS absorption, cache with consistent hashing inside each PoP plus a shield tier for origin offload, and treat config, purge, and certificate distribution as one global, sequence-numbered, staged distribution system. Purges are O(1) via generation numbers and safe for PoPs that reconnect. The biggest risk is a bad global config change, so every config change is staged by PoP ring with automatic rollback. Logs are aggregated at the edge for billing accuracy without shipping petabytes.
Interviewer follow-ups & strong answers
| Follow-up | Strong answer |
|---|---|
| "How do you cache personalized or authenticated content?" | Mostly you don't. Cache the shared parts (fragments, ESI, or edge-side composition), mark personalized responses private, and never let unkeyed headers affect the response (cache poisoning, CDN guide §19) |
| "Signed URLs make every URL unique. Hit ratio drops to 0?" | Validate the signature at the edge, then exclude the signature parameters from the cache key |
| "How do you know you're actually fast?" | Real-user monitoring (browser beacons) plus synthetic probes from many ISPs; per-country p95 TTFB as the SLO |
| "API acceleration for non-cacheable traffic?" | Terminate TLS near the user, then use warm, pooled, long-lived connections over an optimized backbone to the origin. That saves 1–3 RTTs per request |
| "How would you roll out HTTP/3?" | Advertise it via Alt-Svc or HTTPS DNS records; clients fall back to TCP when UDP is blocked; ramp per PoP while watching error rates |
🌍 Real-world references
- Akamai, "The Akamai Network: A Platform for High-Performance Internet Applications" (2010): the foundational architecture paper (mapping system, tiered distribution, overlay routing). Maggs & Sitaraman, "Algorithmic Nuggets in Content Delivery" (2015): cache-on-second-hit via Bloom filters, consistent hashing, and load balancing at Akamai.
- Cloudflare Quicksilver: replicates configuration to every edge server worldwide within seconds. Unimog (2020): Cloudflare's L4 load balancer.
- Fastly: surrogate keys and instant purge are core product features. The June 8, 2021 outage: a valid configuration change by one customer triggered a latent bug and caused errors across most of Fastly's network for under an hour. That's the "global config" failure domain (§14.3), and why config is staged.
- Netflix Open Connect: the internal-CDN variant, with appliances embedded in ISPs and proactive overnight fills.
- This wiki's CDN guide: runnable Go and nginx implementations of multi-tier caching, consistent hashing, purge by surrogate key, signed URLs, mTLS to origin, and multi-CDN failover.
⚠️ Corner cases
Varyexplosion:Vary: User-Agentcreates thousands of variants per URL and drops the hit ratio. Normalize to a few device classes.- Query-string ordering and tracking parameters:
?a=1&b=2vs?b=2&a=1, andutm_*. Normalize them in the cache key. Set-Cookieon cacheable responses: caching it leaks one user's session to others. Strip it, or don't cache.- Range-request first-byte latency on huge files: use slice-based caching.
- Clock skew between edge and origin affecting
Expires: preferCache-Control: max-age, which is relative. - Negative caching: caching an origin 404 for too long after content is published. Use short negative TTLs and purge on publish.
- Long-lived connections during route changes: an anycast shift moves a client's packets mid-connection to a PoP that doesn't know the connection. Mitigations: stable routing, connection-tracking forwarding between PoPs, or QUIC connection IDs for HTTP/3.