all_lessons / system_design / cases / C41 C41 / C44

DNS, HTTP, browser request lifecycle, and QUIC

You typed a URL and hit enter. Before a single byte of your backend code runs, the network has already spent a budget of round trips — and that budget, not your server, is usually what makes a page feel slow.

Source: Archive Case drill Latency-first
First principle — latency is bounded by the speed of light, not your CPU
A round trip (RTT) to a server 1,500 km away is bounded below by physics: light in fibre travels ~200,000 km/s, so the cable alone costs ~15 ms each way, and real RTTs to a regional server land around 40–60 ms. Every protocol step that requires a question-then-answer exchange — DNS lookup, TCP handshake, TLS handshake, the HTTP request itself — costs at least one full RTT, and these costs are serial: you cannot send the HTTP request until TLS is up, and TLS until TCP is up. So the dominant lever on a cold page load is not "make the server faster" — it is "remove round trips from the critical path." That single idea drives DNS caching, TLS 1.3, QUIC/0-RTT, and the CDN, and it is the spine of this entire case.

0. The interview hinge

The canonical version of this question is blunt: "You typed a URL and the page is slow. Walk the stack and tell me where the time goes." A weak answer recites "DNS, then TCP, then the server sends HTML." A senior answer quantifies each hop, identifies which hops are serial RTTs you can collapse, and names the one place the time actually pools (usually the front-end render, not the network). The hinge is therefore: account for the round trips, then attack them.

Invariant to protect
Correctness here is about availability under partial failure, not data integrity. The invariant: a single failed dependency in the resolution-and-connection chain must not take the site dark. Concretely — a DNS provider outage must be survivable via multiple authoritative providers and tested, low-TTL failover; an expired certificate must be impossible by construction (automated renewal). The chain DNS→TCP→TLS→HTTP is a series circuit: any open link kills the whole load, so each link needs its own redundancy.

1. Clarify the contract

This is a "walk the stack" case, so the contract is an explanation contract, not a CRUD API. The candidate must be able to:

What it need not do: design the origin application, choose a database, or solve auth (that is C40). The scope ends at "first byte arrives and the page renders."

2. Put numbers on the round-trip budget

The load-bearing calculation for this entire case is the RTT budget before first byte. Assume a representative RTT = 50 ms to a regional server, a cold cache, and a modern stack (TCP + TLS 1.3). Each handshake is a serial dependency, so the costs add:

StageRound trips (cold)Time @ 50ms RTTWhat it does
DNS resolution1 RTT (0 if cached)50 msName → IP. Recursive resolver may itself need several queries on a true miss; one RTT to the resolver is the optimistic figure.
TCP handshake1 RTT50 msSYN → SYN-ACK → ACK; the client may send data with the final ACK, so it counts as 1 RTT before it can send.
TLS 1.3 handshake1 RTT50 msClientHello+keyshare → ServerHello+cert+Finished. (TLS 1.2 was 2 RTT — TLS 1.3 already cut one.)
HTTP request → first byte1 RTT50 msGET → server processes → first response byte (TTFB).
Total to first byte (cold)4 RTT~200 msPure network/handshake tax, before any HTML is parsed.

Now toggle the optimisations and watch the budget collapse. Each row removes serial round trips from the critical path:

ScenarioDNSTCP/connTLSRequestRTT total@ 50ms
Cold, TCP+TLS1.311114~200 ms
DNS cached01113~150 ms
QUIC (1-RTT handshake)01 (combined transport+crypto)12~100 ms
QUIC 0-RTT (resumed)00 (resumption ticket)1*1~50 ms
CDN hit, edge nearby (RTT≈10ms)0112~20 ms

*0-RTT sends the GET with the first crypto flight, so the request RTT and the handshake overlap — the data is in flight on the very first packet. Caveat: 0-RTT early data is replayable, so it is only safe for idempotent requests (a GET, never a "transfer $100").

Cold TTFB (TCP+TLS1.3)
~200 ms
QUIC fresh connection
~100 ms
QUIC 0-RTT resumed
~50 ms
Saved by QUIC+0-RTT
~150 ms (3 RTT)

The headline: QUIC plus 0-RTT collapses connection setup from 3 RTT to 0, taking the pre-request tax from ~150 ms down to ~0. At 50 ms RTT that is a 150 ms win on every cold connection — and on a mobile link at 150 ms RTT the same 3 saved round trips are worth 3 · 150 = 450 ms, which is the difference between "instant" and "sluggish." This is why the round-trip count, not bandwidth, is the number to attack first. Bandwidth helps the transfer; round trips dominate the setup, and for a small page the setup is most of the latency. See foundation lesson 02 for why latency and throughput are independent axes and why percentiles (p99) — not the mean — are the number that bites here (DDIA Ch. 1).

3. Define the surface area

The "API" here is the set of protocol exchanges on the path. Each is a question/answer that costs latency and can fail independently.

ExchangeWhy it exists / what it costs
DNS query A/AAAAResolve hostname to IP(s). 0 RTT cached, 1+ RTT cold. Cacheable per TTL.
TCP/QUIC connectEstablish reliable transport + (for QUIC) crypto in one. 1 RTT, or 0 on resumption.
TLS handshakeNegotiate cipher, exchange keys, verify cert chain. 1 RTT in TLS 1.3 (folded into QUIC).
HTTP GET + validatorsThe actual request, carrying cookies and cache validators (If-None-Match/If-Modified-Since). 1 RTT to TTFB; can return 304 Not Modified with no body.

4. Model the data

The "data" on this path is the set of cacheable artifacts and the metadata that governs their staleness. Each lives at a different layer with a different TTL, and getting the TTL wrong is the single most common production bug here.

DNS record (+ TTL)TLS certificate (+ expiry)HTTP cache headersCDN / browser cache entryfingerprinted asset

The mental model: a DNS record cached for its TTL is a replica with a staleness bound — exactly DDIA Ch. 5's replication/TTL trade. A long TTL means more clients read a stale (possibly dead) IP after you fail over; a short TTL means more cache misses and more resolver load. The cache headers (Cache-Control: max-age, ETag) play the same role for assets one layer up. The whole case is really "how stale can each replica be, and what does staleness cost when something changes?"

5. Linearized design — follow one request

Walk the request in the order it actually moves. The bottleneck — serial round trips — falls out naturally.

  1. Resolve the name. The browser checks its own cache, then the OS cache, then asks the configured recursive resolver. On a true miss the resolver walks root → TLD → authoritative — but each of those is itself cached, so the common case is a single RTT (or zero). This is the first place a round trip either disappears (cached) or stalls the whole load (cold + slow resolver).
  2. Open the connection. TCP three-way handshake (1 RTT), or QUIC's combined transport+crypto handshake (1 RTT, or 0 on resumption) directly over UDP.
  3. Secure it. TLS 1.3 (1 RTT) for TCP; folded into the QUIC handshake so there is no separate cost.
  4. Send the HTTP request. GET with cookies, headers, and cache validators. If the client holds a valid ETag and the resource is unchanged, the origin returns 304 with no body — a full RTT spent but a body transfer saved.
  5. Route it. The request lands at the nearest CDN edge / load balancer first. A CDN hit answers from an edge ~10 ms away and the origin never wakes up; a miss proxies back to the origin application server. The CDN is the cheapest RTT reducer because it shrinks every RTT on the path, not just one stage.
  6. Return HTML + assets with a cache policy. The origin sets Cache-Control/ETag so the next visit can skip steps 4–6 entirely.
  7. Render. The browser parses HTML, discovers subresources (CSS, JS, images, fonts), opens further fetches, runs JavaScript, and paints. This is where the second latency budget lives — see the deep dive on where page-load time really goes.
COLD PAGE LOAD — request lifecycle timeline (RTT = 50ms) t=0ms 50 100 150 200 ... render ... │ │ │ │ │ ├─DNS────┤ 1 RTT (0 if cached) ├─TCP────┤ 1 RTT SYN/SYN-ACK/ACK ├─TLS1.3─┤ 1 RTT hello/cert/finished ├─HTTP─┤ 1 RTT GET → first byte (TTFB) ├──HTML parse → fetch CSS/JS → run JS → paint──▶ └────────── ~200ms network tax ──┘ └──── often 300–1500ms of front-end work ────┘ QUIC (0-RTT resumed): t=0ms ├─GET rides the first packet (DNS cached, conn resumed)─┤ ~50ms to first byte

6. Deep dives

(1) HTTP/2 TCP head-of-line blocking vs HTTP/3/QUIC per-stream delivery

HTTP/1.1 forced one request per connection at a time, so browsers opened ~6 parallel TCP connections per host to fake concurrency — 6× the handshakes. HTTP/2 fixed the application layer: it multiplexes many logical streams over a single TCP connection, so one handshake serves the whole page. But it left a subtler problem unsolved. TCP delivers a single, strictly ordered byte stream. If one packet is lost, TCP holds back every byte that arrived after it — across all streams — until the retransmission arrives, because TCP cannot know those later bytes belong to a different, independent HTTP/2 stream. So a single lost packet for the logo image stalls the CSS, the JS, and everything else multiplexed on that connection. That is TCP head-of-line blocking, and on a lossy mobile link it can erase HTTP/2's multiplexing win entirely (DDIA Ch. 8: networks are unreliable; loss and reordering are the normal case, not the exception).

HTTP/3 runs over QUIC, which is built on UDP and implements its own per-stream reliability. QUIC knows the stream boundaries, so a lost packet only stalls its own stream; the other streams keep delivering. The head-of-line blocking moves from "whole connection" down to "one stream," which is exactly where you want it. QUIC also folds the transport and TLS handshakes into one (the 1-RTT setup above), supports 0-RTT resumption, and — because the connection ID is independent of the IP/port 4-tuple — survives a network change (Wi-Fi → cellular) without a new handshake.

HTTP/2 over TCP — one lost packet blocks ALL streams stream A: ▓▓▓░░░░░ ← waiting on retransmit stream B: ▓▓░░░░░░ ✗lost ← B's packet dropped here stream C: ▓░░░░░░░ ← also blocked, though C is fine └─ TCP holds back A,C until B's packet is resent (HoL) HTTP/3 over QUIC — loss isolated to its own stream stream A: ▓▓▓▓▓▓▓▓ ✓ keeps flowing stream B: ▓▓░░░░░░ ✗lost → only B waits for retransmit stream C: ▓▓▓▓▓▓▓▓ ✓ keeps flowing

(2) Long vs short DNS TTL — failover speed vs cache hit rate

A DNS record's TTL is the staleness bound on a replicated value (DDIA Ch. 5). Set it long (say 24 h) and the vast majority of lookups hit a cache — zero DNS RTT, low resolver load, fast. But the cost is brutal on the day you need to move traffic: if a data centre or a load balancer IP dies, every client and resolver that cached the old IP keeps hitting the dead address until its TTL expires. A 24 h TTL means up to 24 h of clients pointed at a corpse. Set it short (say 30–60 s) and failover is near-instant — clients re-resolve within a minute and follow you to the new IP — but you pay with cache misses and a constant stream of DNS queries (more resolver load, an added ~1 RTT more often).

The senior move is to match TTL to volatility: long TTL for stable, anycast-fronted endpoints that rarely move; short TTL for the records you steer for failover or blue/green cutover. In practice you keep failover-critical records at 30–60 s and lean on anycast (one IP advertised from many sites, BGP routes you to the nearest live one) so that failover happens in the routing layer and does not depend on DNS TTL expiry at all. This is the same replication/staleness tension that foundation lesson 06 covers for asset caching — and the defer/cache mechanics for fingerprinted assets are taught there, so I won't re-derive them.

(3) Where page-load time actually goes

Here is the answer that separates seniors: for most real pages, the network handshake is not where the time goes — the front-end is. The ~200 ms cold network tax is real and worth attacking, but once the HTML arrives the browser must parse it, discover and fetch dozens of subresources (often the true bottleneck: 50+ requests, each needing a connection or a CDN round trip), parse and execute JavaScript on the main thread, build the render tree, and paint. A typical breakdown for a content page:

PhaseTypical share of "feels loaded"Lever
DNS + connection setup~100–200 msQUIC, 0-RTT, DNS cache, CDN edge
TTFB (origin compute)~50–300 msCache at CDN, faster backend, SSR
Subresource fetch (CSS/JS/img)~300–800 msFewer/smaller assets, HTTP/2-3 multiplexing, preload, compression
JS parse + execute + render~200–1000 ms+Less JS, code-splitting, defer/async, server-render

So when an interviewer says "the page is slow," the disciplined walk is: (1) is DNS cold or the resolver slow? (2) is this a fresh connection paying 3 setup RTTs we could collapse with QUIC/keep-alive? (3) is TTFB high — i.e. is the origin slow, fixable with a CDN cache hit? (4) or — most often — is the network fine and the page is shipping 2 MB of render-blocking JavaScript? You cannot know without splitting the budget; the failure of the weak candidate is assuming the slowness is "the server."

7. Trade-offs

ChoiceBuysCostsChoose when
Long DNS TTL vs short TTLHigh cache hit, low resolver load, fewer RTTsSlow failover — stale clients hit a dead IP for up to the TTLStable, anycast-fronted endpoints that rarely move
HTTP/3 (QUIC) vs HTTP/2 (TCP)Per-stream delivery (no TCP HoL), 1-RTT/0-RTT setup, connection migrationUDP sometimes blocked/throttled by middleboxes; younger stackLossy / mobile networks; you control client and server; always with a TCP fallback
0-RTT resumption vs always full handshakeSaves a full RTT on reconnectEarly data is replayable — unsafe for non-idempotent requestsIdempotent GETs to your own QUIC endpoints
Aggressive asset cache vs quick deployFast repeat visits, near-zero origin loadClients can run stale code after a deployFingerprinted (content-hashed) filenames make this safe
Redirects vs direct canonical URLFlexible routing / migrationEach redirect adds a full DNS+conn+RTT round tripTemporary migration windows only — never on the hot path

The sharpest trade is HTTP/3 vs HTTP/2, because the "obvious" answer (newer is better) is incomplete. QUIC's per-stream delivery is a clear win on lossy links — but it rides UDP, and some corporate firewalls and carrier middleboxes throttle or block UDP, or simply haven't been tuned for QUIC's higher packet rates. The correct posture is not "switch everything to HTTP/3"; it is "advertise HTTP/3 via Alt-Svc, let capable clients upgrade, and keep HTTP/2-over-TCP as the fallback for everyone else." You get QUIC's win where the network allows it and lose nothing where it doesn't.

8. Failure modes

FailureWhy it bitesMitigation
DNS provider outageSeries-circuit link 1 fails → entire site unreachable, regardless of origin healthMultiple authoritative providers, low TTL on failover records, tested failover
Certificate expiryTLS handshake fails for every client at once — a self-inflicted total outageAutomated renewal (ACME), expiry monitoring + alert well before the date
Redirect loopEach hop is a full round trip; a loop burns RTTs and never loadsCap redirect depth, test canonicalization rules
Stale DNS after failoverLong TTL keeps clients pinned to the dead IPShort TTL on steered records + anycast so routing fails over independent of TTL
Cache serves bad / old assetAggressive cache + reused filename → clients run broken old JS after deployContent-hashed (fingerprinted) filenames; rollback-safe deploys
QUIC/UDP blocked by middleboxConnection silently stalls if there's no fallbackAdvertise via Alt-Svc; always retain HTTP/2-over-TCP fallback

9. Interview prompts you should be ready for

  1. "You typed a URL and the page is slow — walk the stack and tell me where the time goes." (senior answer) Split the budget. Cold network tax is ~4 RTT ≈ 200 ms at 50 ms RTT: DNS (1, or 0 cached), TCP (1), TLS 1.3 (1), HTTP→TTFB (1). Then ask: is the connection fresh (3 collapsible setup RTTs) or reused? Is TTFB high (slow origin — put a CDN cache in front)? But most often the network is fine and the time is in the front end — 50+ subresource fetches and megabytes of render-blocking JS parsing on the main thread. Name the phase before prescribing a fix.
  2. Why does HTTP/2 still suffer head-of-line blocking when it multiplexes? (senior answer) The multiplexing is at the HTTP layer, but it still rides a single TCP connection, and TCP delivers one strictly-ordered byte stream. One lost packet makes TCP hold back every later byte across all streams until the retransmit lands. HTTP/3 fixes it by running over QUIC, which tracks stream boundaries itself so loss only stalls its own stream.
  3. What exactly does QUIC/0-RTT save, in round trips? (senior answer) A fresh QUIC connection folds transport+crypto into one 1-RTT handshake (vs TCP's 1 + TLS 1.3's 1 = 2). On resumption, 0-RTT sends the GET in the very first packet — 0 setup RTTs. So worst-case you go from 3 setup RTTs to 1, best-case to 0; at 50 ms that's 100–150 ms saved per cold connection, and proportionally more on mobile. Caveat: 0-RTT early data is replayable, so only for idempotent requests.
  4. How do you choose a DNS TTL? (senior answer) It's a staleness-vs-load trade. Long TTL maximises cache hits and minimises resolver load but pins clients to a dead IP for up to the TTL on failover. Short TTL gives near-instant failover at the cost of more misses/queries. Keep failover-critical records at 30–60 s, stable records long, and lean on anycast so failover happens in routing, independent of TTL expiry.
  5. A user's first visit is fast but they complain the site is "slow" — what's likely? (senior answer) If repeat visits feel slow, suspect cache misconfiguration (no Cache-Control/ETag, so every asset re-downloads). If the first visit is slow, suspect cold-connection RTTs or a slow origin TTFB. Always confirm whether the bottleneck is network setup, origin compute, or front-end render before changing anything.
  6. Why not just put a CDN in front and call it done? (senior answer) A CDN is the highest-leverage move — it shrinks every RTT (the edge is ~10 ms away) and absorbs the origin load for cacheable content. But it only helps cacheable, mostly-static responses; personalised/dynamic responses still go to origin, and you've added a cache-invalidation problem (fingerprint assets, set sane TTLs). It reduces the network tax dramatically but does nothing for a heavy-JS front end.
  7. Should you turn on HTTP/3 everywhere? (senior answer) No — advertise it via Alt-Svc and let capable clients upgrade, but keep HTTP/2-over-TCP as fallback. QUIC rides UDP, which some middleboxes throttle or block; without a fallback those clients stall. You want the win where the network allows it and zero regression where it doesn't.
  8. Where does this connect to load and capacity? (senior answer) The same queueing math from lesson 02 applies at the edge: as a CDN node or resolver nears saturation (ρ→1) tail latency blows up superlinearly, and that p99 is what users feel (DDIA Ch. 1). The network-unreliability assumptions — loss, reordering, partition — are DDIA Ch. 8, and they're exactly what TCP HoL blocking and QUIC's per-stream recovery are reacting to.

Related foundation lessons

Three foundation lessons carry the load here, and I cite rather than re-derive them. Lesson 02 (latency & throughput) owns the RTT/latency math and the percentile argument — the entire round-trip budget above is an application of it. Lesson 06 (caching) owns the hit-rate→origin-load and TTL/staleness mechanics; the asset-cache and defer mechanics live there, so this case covers only the DNS-TTL and CDN-edge deltas. Lesson 15 (rate limiting & the edge) is the companion for what else happens at that same edge — TLS termination, admission control, and why the front door is the cheapest place to enforce cross-cutting concerns.

Latency and throughput Caching Rate limiting and edge Observability
Takeaway
After you type a URL, the network spends a fixed budget of serial round trips before your code runs: ~4 RTT (DNS + TCP + TLS 1.3 + request) ≈ 200 ms at 50 ms RTT. The job of every optimisation on this path — DNS caching, TLS 1.3, QUIC, 0-RTT, the CDN edge — is to remove round trips from the critical path; QUIC+0-RTT alone collapses 3 setup RTTs to 0. Compare protocols by where head-of-line blocking lives (whole-connection in HTTP/2-over-TCP, per-stream in HTTP/3-over-QUIC), and treat DNS TTL as a replication/staleness knob (failover speed vs cache hit rate). But the senior point is to split the budget: most "slow page" time is not the handshake at all — it's the front end parsing and executing the JavaScript the network worked so hard to deliver.