DNS & CDN
At 10:00 the BoxOffice stadium sale opens and 2 million phones ask the same two questions almost at once: "where is boxoffice.com?" and "give me the event page". If both questions travel all the way to the BoxOffice servers in SΓ£o Paulo, the servers drown before a single seat is sold. The Domain Name System (DNS) and a Content Delivery Network (CDN) exist so that most of those questions are answered close to the fan, by machines that BoxOffice does not even operate.
This page covers the first two hops of the request's journey: DNS decides where a client goes; the CDN decides whether the request needs to go any further.
DNS: the Internet's phone book
Computers talk to IP addresses (203.0.113.10, 2001:db8::10); people type names (boxoffice.com). DNS translates the name into addresses. It is one of the oldest and largest distributed systems in existence: a hierarchical, delegated, heavily cached database1.
- Hierarchical: names are read from right to left. The root (
.) knows who is responsible forcom.;com.knows who is responsible forboxoffice.com.;boxoffice.com.knows the address ofapi.boxoffice.com. - Delegated: each level is run by a different organisation. BoxOffice controls only its own zone,
boxoffice.com, through its authoritative name servers (usually a managed service such as Route 53 or Cloudflare DNS). - Cached: every answer carries a TTL (time to live, in seconds). Resolvers keep the answer for that long, so the vast majority of lookups never reach the authoritative servers.
Resolution, step by step
When the fan's phone needs api.boxoffice.com, it does not walk the hierarchy itself. A tiny stub resolver in the operating system asks a recursive resolver (the one from the mobile carrier, or a public one such as 1.1.1.1 or 8.8.8.8), which does the work and caches the results:
sequenceDiagram
participant P as Phone (stub resolver)
participant R as Recursive resolver
participant Root as Root server
participant TLD as .com TLD server
participant A as boxoffice.com authoritative
P->>R: A? api.boxoffice.com
Note over R: cache miss
R->>Root: A? api.boxoffice.com
Root-->>R: ask .com (NS a.gtld-servers.net)
R->>TLD: A? api.boxoffice.com
TLD-->>R: ask boxoffice.com (NS ns1.boxoffice-dns.net)
R->>A: A? api.boxoffice.com
A-->>R: 203.0.113.10, TTL 60
Note over R: cache for 60 s
R-->>P: 203.0.113.10
P->>R: A? api.boxoffice.com (another fan, 5 s later)
R-->>P: 203.0.113.10 (from cache) A cold lookup costs several round trips (tens to hundreds of milliseconds); a cached one costs a single round trip to the resolver, or nothing at all when the answer is in the operating system or browser cache. In practice the root and .com answers are almost always cached too (their TTLs are one to two days), so a typical miss costs just one trip from the resolver to the authoritative server.
Think first: 2 million fans look up boxoffice.com at 10:00. How many queries reach BoxOffice's authoritative servers?
Far fewer than 2 million. Fans share a few thousand recursive resolvers (their mobile carriers, ISPs and public resolvers). Each resolver asks the authoritative server once per TTL and serves everyone else from its cache. With a 60 s TTL and, say, 5 000 active resolvers, the authoritative servers see on the order of 5 000 queries per minute β about 80 per second. DNS caching is the first and most effective load shield on the Internet.
Record types
| Type | Maps | Example | Used for |
|---|---|---|---|
| A | name β IPv4 address | api.boxoffice.com β 203.0.113.10 | the basic lookup |
| AAAA | name β IPv6 address | api.boxoffice.com β 2001:db8::10 | IPv6 clients (most mobile networks) |
| CNAME | name β another name (alias) | www.boxoffice.com β d123.cloudfront.net | pointing at a CDN or load balancer whose IPs change |
| ALIAS / ANAME | apex name β another name (provider feature) | boxoffice.com β my-alb.amazonaws.com | a CNAME is not allowed at the zone apex; providers flatten it |
| NS | zone β its name servers | boxoffice.com β ns1.boxoffice-dns.net | delegation |
| MX | domain β mail servers | boxoffice.com β mail.provider.com | receiving e-mail |
| TXT | name β free text | v=spf1 include:β¦ | domain ownership proofs, SPF/DKIM/DMARC for e-mail |
| SRV | service β host + port | _sip._tcp.boxoffice.com | service discovery (Kubernetes DNS uses it internally) |
| CAA | domain β allowed certificate authorities | 0 issue "letsencrypt.org" | restricts who may issue TLS certificates for the domain |
TTL: freshness versus load
The TTL is a classic trade-off between how fast a change propagates and how much load and latency the lookups cost:
| TTL | Change propagates in | Lookup load / latency | Good for |
|---|---|---|---|
| 5β60 s | about a minute | many more queries; more cold lookups for users | failover records, records you plan to change |
| 5β60 min | about an hour | low | ordinary records |
| 1β2 days | days | minimal | NS records, things that never change |
Lower the TTL before a migration
If you are going to move api.boxoffice.com to a new load balancer next Tuesday, lower its TTL from 1 hour to 60 seconds on Monday (at least one old TTL in advance). Resolvers that cached the old record with the 1-hour TTL will have expired it by Tuesday, and the switch will take about a minute instead of an hour. Raise the TTL again after the migration.
Routing with DNS: GeoDNS, latency, weights and failover
Because the authoritative server chooses the answer, it can give different answers to different clients. Managed DNS services offer several routing policies4:
| Policy | The answer depends on | BoxOffice use |
|---|---|---|
| Simple | nothing; always the same records | development environments |
| Geolocation (GeoDNS) | the client's country or continent (estimated from the resolver's IP, or from the EDNS Client Subnet option) | send fans in Europe to the EU region for data-residency rules |
| Latency-based | the region with the lowest measured latency from the client's network | SΓ£o Paulo fans to sa-east-1, Lisbon fans to eu-west-1 |
| Weighted | a configured percentage | send 5% of traffic to a new region or stack (DNS-level canary) |
| Failover | the result of a health check | the secondary region answers only when the primary is unhealthy |
DNS failover is the simplest form of multi-region disaster recovery: the DNS provider probes https://sa.boxoffice.com/health every 10β30 s from several locations; after a few failures it stops returning the primary's address and returns the secondary's.
sequenceDiagram
participant HC as DNS health checker
participant P as Region SP (primary)
participant S as Region US (secondary)
participant F as Fan's resolver
HC->>P: GET /health
P-->>HC: 200 OK
F->>HC: A? api.boxoffice.com
HC-->>F: SP address (TTL 60)
Note over P: region outage
HC->>P: GET /health
Note over HC: 3 failures in a row: mark SP unhealthy
F->>HC: A? api.boxoffice.com (after its TTL expired)
HC-->>F: US address (TTL 60)
F->>S: traffic now goes to the secondary DNS failover is not instant
The time to fail over is roughly detection + TTL: with a 30 s health-check interval, 3 failures to declare the primary down and a 60 s TTL, clients move after about 90 s + 60 s = 2.5 minutes in the best case. In practice it is longer: some resolvers and clients ignore low TTLs (ISP resolvers that enforce a minimum TTL, applications that resolve a name once and keep the address in a connection pool, older devices), and long-lived connections keep talking to the dead region until they break. DNS failover is good for regional disasters; for failing servers inside a region, use a load balancer with health checks, which reacts in seconds (Load Balancing).
Try it. Open the multi-region scenario and run the load profile. Then trigger the outage of the primary region (chaos high, or disable the region in the panel). Watch how long errors last before DNS sends traffic to the secondary, and change the DNS TTL to see how it stretches or shortens that window.
Anycast
With ordinary (unicast) addressing, one IP address belongs to one place. With anycast, the same IP address is announced over BGP (the Border Gateway Protocol that glues the Internet's networks together) from many locations at once, and each packet is delivered by the network to the nearest one, in routing terms.
flowchart LR
f1([Fan in SΓ£o Paulo]) --> sp["Edge SΓ£o Paulo<br/>announces 198.51.100.1"]
f2([Fan in Lisbon]) --> li["Edge Lisbon<br/>announces 198.51.100.1"]
f3([Fan in Tokyo]) --> tk["Edge Tokyo<br/>announces 198.51.100.1"] Anycast gives three things at once, without any DNS trick:
- Low latency: every client reaches a nearby location.
- Automatic failover: if a location stops announcing the address, routing converges to the next-nearest one, typically within seconds.
- DDoS absorption: an attack from a global botnet is split among all the locations instead of hitting one (see edge security).
The root DNS servers, public resolvers like 1.1.1.1 and 8.8.8.8, and every large CDN use anycast. Anycast works best with short, mostly stateless exchanges (DNS over UDP, HTTP requests at an edge cache); a route change in the middle of a long TCP connection can break it, which large providers mitigate with careful routing.
CDN: bring the content to the fan
A CDN is a network of caching reverse proxies β points of presence (PoPs), hundreds of them around the world β that store copies of your content close to users3. A fan in Recife who asks for the BoxOffice logo receives it from a PoP in Recife or Fortaleza, not from the origin in SΓ£o Paulo; a fan in Lisbon receives it from Lisbon, not from across the Atlantic.
A CDN does three jobs at once5:
- Latency: content travels a few kilometres instead of thousands. A round trip across the Atlantic costs around 100β150 ms; to a PoP in the same city, a few milliseconds.
- Offload: every cache hit is a request the origin (your servers) never sees. This is often the main reason for a CDN in a system design.
- Protection: the CDN's capacity and anycast network absorb traffic spikes and attacks before they reach you.
sequenceDiagram
participant F1 as Fan 1 (Recife)
participant E as CDN PoP Recife
participant O as Origin (SΓ£o Paulo)
participant F2 as Fan 2 (Recife)
F1->>E: GET /img/stadium.webp
Note over E: MISS: not in cache
E->>O: GET /img/stadium.webp
O-->>E: 200, Cache-Control: max-age=86400
E-->>F1: 200 (stored for 1 day)
F2->>E: GET /img/stadium.webp
Note over E: HIT
E-->>F2: 200 from the PoP (origin untouched) Pull versus push
| Model | How content gets to the PoPs | Pros | Cons | Good for |
|---|---|---|---|---|
| Pull (origin pull) | the PoP fetches from the origin on the first miss, then caches it | zero upload work; only popular content occupies the cache | the first request in each PoP is slow (a miss) | websites, APIs, almost everything |
| Push | you upload content to the CDN storage in advance | no cold misses; predictable | you manage uploads, versions and deletion | large files known in advance: video, software downloads, game patches |
Most designs use pull in front of an origin, and that origin is frequently an object store (S3, Cloud Storage, Azure Blob β the objstore component) for static files and media.
The cache key
The CDN stores each response under a cache key β by default, the host plus the URL path plus the query string. Two requests with the same key share the cached copy. Getting the key right is most of the work:
- Too specific a key fragments the cache:
/events/42?utm_source=instagramand/events/42?utm_source=emailare the same page, but with the default key they are cached twice and both miss. Remove tracking parameters from the key, or sort query parameters. - Too generic a key leaks data: if the response depends on a cookie (
session=β¦) or a header (Accept-Language) that is not in the key, one fan may receive a page personalised for another. - The
Varyresponse header tells caches which request headers change the response (Vary: Accept-Encodingis normal;Vary: Cookieeffectively disables shared caching).
Cache-Control: the origin decides
The origin tells every cache β the browser, the CDN, any proxy in between β what it may do with a response through the Cache-Control header2:
| Directive | Meaning |
|---|---|
max-age=N | the response is fresh for N seconds (in any cache) |
s-maxage=N | freshness for shared caches (CDN, proxies) only; overrides max-age there |
public / private | may be stored by shared caches / only by the user's browser |
no-cache | may be stored, but must be revalidated with the origin before each use |
no-store | must not be stored anywhere (personal data, payment pages) |
stale-while-revalidate=N | after expiry, serve the stale copy for up to N s while fetching a fresh one in the background |
stale-if-error=N | if the origin fails, keep serving the stale copy for up to N s |
immutable | the content will never change at this URL; do not even revalidate |
Revalidation uses validators: the origin sends an ETag (a version identifier) or Last-Modified; the cache asks If-None-Match: "v42", and the origin answers 304 Not Modified with no body when nothing changed β cheap for both sides.
# fingerprinted JavaScript bundle: cache "forever"
Cache-Control: public, max-age=31536000, immutable
# event page HTML: short in browsers, longer at the CDN, survive origin hiccups
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=3600
# seat availability JSON during the sale: micro-cache at the CDN for one second
Cache-Control: public, max-age=0, s-maxage=1, stale-while-revalidate=1
# the fan's order and payment pages: never cache
Cache-Control: private, no-store
Invalidation: purge or version?
The event's poster image changed. How do the 300 PoPs learn about it? There are two strategies:
- Purge (invalidation): call the CDN API to remove
/img/stadium.webp(or a tag, or a prefix) from every PoP. The next request misses and fetches the new version. Purges are not instantaneous β they propagate in seconds to minutes, depending on the provider β and browsers that already cached the old version withmax-agewill not be purged at all. - Versioned (fingerprinted) URLs: never change a file in place. The build produces
app.3f9a1c.js; a new build producesapp.b71e02.jsand the HTML references the new name. Old and new versions coexist, caches never serve the wrong one, and the files can be cached withimmutablefor a year. Only the small HTML that references them needs a short TTL.
Prefer versioned URLs for every static asset and keep purges for the exceptional cases (an error on a published page, legal take-downs). This is the edge version of the oldest joke in computing β there are only two hard things in computer science: cache invalidation and naming things β and versioned URLs solve the first by doing the second. Invalidation in general is studied in the caching class.
Origin shield and tiered caching
With 300 PoPs, a new file can cause 300 misses β one per PoP β that all hit the origin. A tiered cache (or origin shield) adds a middle layer: PoPs ask a regional cache, and only the regional cache asks the origin. The origin sees one request per object instead of one per PoP, and request collapsing at the shield merges simultaneous misses for the same object into a single origin fetch.
flowchart LR
p1[PoP Recife] --> sh[Origin shield<br/>SΓ£o Paulo]
p2[PoP Porto Alegre] --> sh
p3[PoP Manaus] --> sh
sh --> o[(Origin)] How much does the hit ratio matter?
The fraction of requests the CDN serves from cache is the hit ratio. The load on the origin is the miss fraction times the traffic, so every extra "nine" of hit ratio divides the origin load by ten. Suppose (illustrative assumption) 200 000 requests per second reach the edge during the sale:
Going from 90% to 99% hit ratio looks like a small improvement ("only nine points"), but it cuts the origin load from 20 000 to 2 000 requests per second β ten times fewer servers. When you discuss a CDN in an interview, talk about the miss rate, not the hit rate: it is the number that sizes the origin.
Try it. Load the CDN scenario and run the load profile. Lower the CDN hit ratio (or the TTL) step by step and watch the request rate and p99 latency at the app servers grow; then raise it and see how few app replicas you need. Finally, purge the cache during the run: the sudden wave of misses is a small-scale stampede on the origin.
Caching dynamic content: micro-caching
"Dynamic" does not mean "uncacheable". The seat-availability JSON of the stadium show changes all the time, but nobody is harmed if it is one second old. If every PoP caches it for 1 s (s-maxage=1), the origin receives at most one request per second per PoP (or per shield, with tiered caching), no matter how many fans are polling. With 2 million fans polling every 5 seconds β 400 000 requests per second β and 50 PoPs serving Brazil, the origin sees about 50 requests per second for that endpoint. The seat map is at most a second stale; the hold itself (POST /api/holds) is never cached and is checked against the database, so a stale map can never cause a double booking.
Edge compute
Modern CDNs also run your code in the PoPs: Cloudflare Workers, AWS CloudFront Functions and Lambda@Edge, Fastly Compute, Akamai EdgeWorkers. These are small functions (milliseconds of CPU, limited memory, usually JavaScript or WebAssembly) executed on each request, before the cache or the origin. Good uses:
- Validate a waiting-room token or a JWT signature at the edge and reject invalid requests before they cross the ocean.
- Rewrite and redirect: A/B tests, language redirects, normalising the cache key (removing
utm_*parameters). - Assemble responses from cached fragments, or add security headers.
- Serve a waiting-room page entirely from the edge while the origin is protected.
Bad uses: anything that needs your database on every request (the function is far from the data, so each call crosses the network anyway), long computations, and business logic that would be better tested and versioned in a service.
- DNS: Route 53 (latency, geolocation, weighted and failover routing; health checks).
- CDN: CloudFront, with CloudFront Functions and Lambda@Edge for edge compute; Origin Shield.
- Origin: S3 for static files and media; Application Load Balancer for APIs.
- Protection: AWS WAF and AWS Shield (Standard is included; Advanced is paid).
- DNS: Cloud DNS (with geolocation and failover routing policies).
- CDN: Cloud CDN, attached to the global external Application Load Balancer; Media CDN for video.
- Origin: Cloud Storage buckets or backend services behind the load balancer.
- Protection: Cloud Armor (WAF rules and DDoS protection).
- DNS: Azure DNS; Traffic Manager for DNS-based routing (performance, priority, weighted, geographic).
- CDN: Azure Front Door (global L7 entry point with CDN, WAF and routing).
- Origin: Blob Storage, App Service, AKS behind Application Gateway.
- Protection: Azure WAF and Azure DDoS Protection.
- DNS: BIND, PowerDNS, CoreDNS (the DNS inside Kubernetes); managed: Cloudflare DNS, NS1.
- CDN: Cloudflare, Akamai, Fastly, Bunny; self-hosted caching with Varnish or Nginx
proxy_cache. - Edge compute: Cloudflare Workers, Fastly Compute, Akamai EdgeWorkers.
Trade-offs at a glance
| Decision | Option A | Option B | Choose A when⦠|
|---|---|---|---|
| DNS TTL | short (30β60 s) | long (hours) | you need fast failover or plan a change |
| Routing | latency-based / GeoDNS | anycast | you need per-region control (data residency, canaries) |
| Failover | DNS failover | anycast / global LB | a few minutes of failover time is acceptable and cheap matters |
| CDN model | pull | push | content is requested unpredictably (most web traffic) |
| Freshness | purge on change | versioned URLs | the URL cannot change (the HTML page itself, an API) |
| Dynamic data | micro-cache (1β5 s) | no cache | slightly stale data is acceptable and the endpoint is hot |
| Logic | edge function | origin service | the logic is small, stateless and must run before the origin |
BoxOffice: DNS and CDN for the stadium sale
boxoffice.comandwwwβ ALIAS/CNAME to the CDN, which answers from anycast addresses.api.boxoffice.comhas latency-based records for the SΓ£o Paulo and Virginia regions, with failover health checks and a 60 s TTL.- Static assets (JavaScript, CSS, fonts) are fingerprinted and cached for a year with
immutable. Event posters live in the object store and are cached for a day; the rare change uses a new file name. - The event page HTML:
s-maxage=300, stale-while-revalidate=60, stale-if-error=3600. If the origin goes down during the sale, fans still see the page. - Seat availability: micro-cached for 1 s at the CDN, with an origin shield in SΓ£o Paulo. Holds, orders and payments:
private, no-store, never cached, always go to the origin. - An edge function checks the waiting-room token on every
/api/*request; requests without a valid token receive the waiting-room page from the edge.
Back-of-the-envelope: 2 M fans Γ (1 page + 30 assets) at the opening, plus polling, is millions of requests per minute; with a 99% hit ratio on assets and micro-caching on the seat map, the origin sees a few thousand requests per second, almost all of them holds and orders β the requests that actually need the origin.
In the interview
- Say what you cache at the CDN and for how long: "static assets and images with versioned URLs and long TTLs; the event page for minutes with
stale-while-revalidate; the seat map micro-cached for 1 s; nothing user-specific." - Quantify with the miss rate: "at 99% hit ratio, 200 k req/s at the edge become 2 k at the origin."
- Mention DNS in one sentence β "latency-based DNS to the nearest region, with health-checked failover" β and know its limit: failover takes about detection + TTL, minutes, not seconds.
- For a global, read-heavy system (news feed, video streaming, URL shortener redirects), the CDN is often the most important component of the design.
Best practices
- Set
Cache-Controlexplicitly on every origin response β includingprivateorno-storeon personal data; never let a CDN's default decide what is shared. - Lower the DNS TTL days before a planned migration or sale, and raise it afterwards β resolvers keep the old TTL until it expires, so the change must be made a TTL ahead.
- Monitor the hit ratio per path and alert when it drops β from 95% to 80% quadruples the origin's load (5% β 20% of requests).
- Serve
stale-if-errorandstale-while-revalidateβ the CDN keeps answering with the last good copy when the origin fails or is refreshing. - Keep the static fallback pages on the CDN β "sold out", the waiting room, the maintenance page; they must load when every service behind them is down.
Pitfalls
- Caching personalised responses in a shared cache because a cookie or
Authorizationheader is not in the cache key: one user sees another's data. Mark themprivateorno-store. - Purging as a deploy strategy: purges are slow, can fail and do not reach browsers. Use versioned file names.
- Cache-busting by accident: random query parameters, timestamps in URLs or
Vary: Cookiesilently drop the hit ratio to zero. - Thundering herd on expiry: if a hot object expires in every PoP at the same time, they all miss together. Origin shield, request collapsing and
stale-while-revalidateprevent it. - Trusting DNS failover for fast recovery: low TTLs are not always respected; long-lived connections do not re-resolve.
- Forgetting that the CDN is a dependency: a CDN outage takes your site down too. Very large sites use two CDNs (multi-CDN) with DNS steering between them.
How it connects
flowchart LR
TTL["DNS TTL"] --> FO["failover speed<br/>β multi-region (class 3)"]
ANY["anycast"] --> DDOS["DDoS absorption<br/>β edge security"]
GEO["GeoDNS / latency routing"] --> HOME["home region per event<br/>β CAP & PACELC"]
HIT["CDN hit ratio"] --> ORIGIN["origin load<br/>β Estimation"]
CC["Cache-Control, TTL"] --> STALE["staleness = eventual consistency<br/>β Replication & Consistency"]
INV["invalidation, versioned URLs"] --> CACHE["cache invalidation<br/>β Caching"]
EC["edge compute"] --> WR["waiting-room token check<br/>β API gateway"] Palette components: dns Β· cdn Β· objstore (the usual CDN origin for images and video) Β· cache (the next cache layer behind the CDN). Scenarios: cdn-static, multi-region, video-streaming. Continues in: Caching β patterns, Distributed Systems β CAP & PACELC, Case study: video streaming.
Key takeaways
- DNS is a hierarchical, delegated, cached database; caching by recursive resolvers means the authoritative servers see one query per resolver per TTL.
- The TTL trades change propagation against lookup load; lower it before a planned change.
- DNS can route by geography, latency, weight or health; DNS failover takes about detection + TTL, and some clients ignore TTLs.
- Anycast announces the same IP from many locations: low latency, fast failover and DDoS absorption.
- A CDN cuts latency, offloads the origin and absorbs attacks; most CDNs pull from the origin and cache by a cache key you must design.
- The origin controls caching with
Cache-Control; prefer versioned URLs to purges; micro-caching makes hot dynamic endpoints cheap. - Every extra nine of hit ratio divides the origin load by ten.
-
MOCKAPETRIS, P. RFC 1034 β Domain Names: Concepts and Facilities and RFC 1035 β Implementation and Specification. IETF, 1987.Β β©
-
FIELDING, R.; NOTTINGHAM, M.; RESCHKE, J. RFC 9111 β HTTP Caching. IETF, 2022. See also NOTTINGHAM, M. RFC 5861 β HTTP Cache-Control Extensions for Stale Content, 2010.Β β©
-
CLOUDFLARE. What is a CDN? and What is anycast? β Cloudflare Learning Center.Β β©
-
AWS. Amazon Route 53 β Choosing a routing policy. AWS Documentation.Β β©
-
XU, A. System Design Interview β An Insider's Guide, vol. 1, ch. 1 (CDN, DNS). Byte Code LLC, 2020.Β β©