Frontend Labs
⋯

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) and stale-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

  • Vary explosion: Vary: User-Agent creates 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=2 vs ?b=2&a=1, and utm_*. Normalize them in the cache key.
  • Set-Cookie on 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: prefer Cache-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.

Generated from real-life-system-design-guide.md · 8 Oct 2026 16:36