TogetherOS + Networkinglatency budgetDNSTCPTLSRTT

Capstone: Three Seconds from Warsaw

A user in Warsaw opens your application hosted in another region and the page takes three seconds: enumerate every layer where the time could be, assign each to the domain that explains it, put a typical cost and a measurement next to each, and bisect.

ConceptualSimulated
▶ InteractiveInterview question
Progress

The problem

A user in Warsaw opens your application, hosted in a region 100 ms away. The page takes 3 seconds to load. Explain every layer where the latency could originate, what each typically costs, how you would measure it, and how you would find the one that is actually responsible — without guessing.

The shape of the answer

Conceptual

Three seconds is enormous. A single round trip Warsaw ↔ Frankfurt is ~20–30 ms; Warsaw ↔ US-East is ~100–120 ms; Warsaw ↔ Singapore ~180 ms. A well-built page from a distant region needs roughly: DNS (0–1 RTT to a resolver, more if uncached), TCP handshake (1 RTT), TLS 1.3 (1 RTT; TLS 1.2 is 2), the request and first byte of response (1 RTT plus server time), then the body at a rate that ramps with cwnd — perhaps 5–8 RTTs before the base document is in. At 100 ms that is 500–800 ms with a fast server. Three seconds means either many more round trips than necessary, a server that spends seconds, a loss-degraded path, or a page that needs dozens of dependent fetches.

The wrong answer is a list of technologies. The right answer is a budget: every layer named, in order, with a typical cost and a measurement, and then a bisection — measure the boundaries between layers until the missing seconds are found. The matrix below is the budget; the following section is the bisection. Each row names the Engineer Atlas domain whose lessons explain it, because the point of this capstone is that no single domain can answer the question.

Where the time can go, in order
  1. Browser: DNS lookupcache → OS resolver → recursive → authoritative; 0 to hundreds of ms
  2. TCP handshake1 RTT; or 0 with a reused connection
  3. TLS handshake1 RTT (1.3), 2 RTT (1.2); 0-RTT resumption possible
  4. Request in flight; CDN / LBedge hit ends here; miss adds edge→origin RTT + connection
  5. Server: accept, schedule, runbacklog wait, thread/loop availability, CPU time
  6. Server → databaseconnection (pooled?), query, disk I/O, result size
  7. Response body transfersize / (bandwidth) plus cwnd ramp: log2(size/initcwnd) RTTs
  8. Browser: parse, render, dependent fetcheseach dependent resource repeats the top of the ladder

The latency budget

Conceptual

Typical costs are for a 100 ms RTT path in a healthy state; they are orders of magnitude for reasoning, not measurements of your system. The measurement column is the tool that isolates that layer and nothing else — the property that makes a bisection possible. The lesson column is where to go when a row is the answer.

Read the domain column as a routing table for the investigation. Rows owned by Networking cost round trips and are fixed by removing round trips (caching, reuse, proximity); rows owned by Operating Systems cost waiting and are fixed by removing queues (capacity, pools, non-blocking I/O); rows owned by Database cost work and are fixed by doing less of it (indexes, fewer queries, smaller results). A three-second page usually has one row an order of magnitude over budget, and the domain tells you which kind of fix to expect before you have found it.

Latency budget: area → domain → typical cost → how to measure → lesson (costs illustrative, 100 ms RTT)
AreaDomainTypical costHow to measureLesson
DNS resolutionNetworking0 ms cached; 20–300 ms uncached; seconds on a failing resolverdig +trace; curl -w %{time_namelookup}; browser devtools DNS phaseFollowing One Lookup Through Every Cache, DNS Failure Modes: What Each One Looks Like
TCP connectionNetworking1 RTT ≈ 100 ms; 0 if pooled/keep-alive; +1–3 s on SYN losscurl -w %{time_connect} − %{time_namelookup}; devtools "Initial connection"The Three-Way Handshake, Keep-Alive and Connection Reuse
TLS handshakeNetworking / Security1 RTT (1.3) ≈ 100 ms; 2 RTT (1.2); +OCSP fetch if stapling is offcurl -w %{time_appconnect} − %{time_connect}; openssl s_client -msgThe TLS Handshake, Certificates and the Chain of Trust
Network RTTNetworking20–30 ms EU; 100–120 ms EU↔US-East; 180 ms EU↔SGping / mtr from the user’s location; ss -ti rtt on the serverLatency: Same Machine to Cross-Continent, Bandwidth vs Latency
Packet lossNetworking1% loss: 2–10× slower transfers; RTO ≥ 200 ms per event, doublingmtr loss column; ss -ti retrans; nstat TcpRetransSegsPacket Loss: Duplicate ACKs, Fast Retransmit and the RTO, Congestion Control: Protecting the Network
CDNSystem Design / Networkinghit: user↔edge RTT ≈ 10–30 ms; miss: + edge↔origin RTT + origin timeresponse headers (x-cache, age); compare edge vs origin curlCDNs: The Networking View
Load balancer / proxySystem Design / Networking1–5 ms; +1 RTT if it opens a new upstream connection per request; 502/504 on unhealthy upstreamLB access-log upstream_time vs total; proxy connection reuse metricsLoad Balancers: L4 vs L7, Forward and Reverse Proxies
Server schedulingOperating Systems<1 ms idle; 10s–100s of ms under CPU saturation or a full accept queuerun-queue length (vmstat r); ss -ltn Recv-Q; scheduler latency histogramsThe Scheduling Problem, Context Switching
Application CPUOperating Systems / DSA1–50 ms typical; seconds for O(n²) code, big JSON, template renderingprofiler (perf, py-spy, async-profiler); per-handler timingFollow a Program: from `./server` to the First Instruction, Dynamic Programming
Thread pool / event loopOperating Systems0 when free; queue-wait or loop-lag otherwise; unbounded under overloadpool queue depth and wait; event-loop lag metric; thread dumpsThe Thread Pool Server, The Event-Driven Server
Database network callNetworking / Databasesame-AZ 0.2–1 ms per round trip; cross-region 100 ms; ×N for N queries (N+1)query count per request; pool wait time; DB client timingsFollow send() Through the OS to recv(), Connection Pooling
Database queryDatabase<1 ms indexed; 10 ms–seconds for scans, sorts, lock waitsEXPLAIN ANALYZE; pg_stat_statements; slow-query logReading EXPLAIN ANALYZE, Why Is This Query Slow? Indexes
Disk I/OOperating Systems / Database~100 µs SSD read; ms for HDD; seconds under a swap storm or fsync queueiostat await; buffer-cache hit ratio; vmstat si/soFollow a File Read, Memory Pressure, Swap and the OOM Killer
Response sizeNetworkinglog2(size / 14 kB) RTTs to ramp cwnd; 1 MB ≈ 6–7 RTTs ≈ 700 ms at 100 mscontent-length; devtools "Content download"; compare gzip/brotliThroughput: Requests, Packets and Bytes per Second, Where the Time Goes: The Request Timeline
Congestion / bufferbloatNetworkingRTT under load ≫ idle RTT; 100s of ms of queueing on a saturated linkmtr under load vs idle; ss -ti cwnd and rtt varianceCongestion Control: Protecting the Network, The Buffer Chain
Browser processingBrowserparse/JS/render 50 ms–seconds; each dependent resource repeats DNS/TCP/TLS unless reuseddevtools Performance and Network waterfall; LighthouseWhat Happens When You Press Enter, HTTP/1.1 vs HTTP/2 vs HTTP/3

Bisecting three seconds

Conceptual

Start from the outside and split. First split: is it the network path or the server? curl -w from the user’s region against the same URL prints every phase — name lookup, connect, TLS, time-to-first-byte, total. If time_starttransfer minus time_appconnect (the server’s think time plus one RTT) is 2.5 s, the server is the problem and the network rows can be closed. If that gap is 150 ms but the total is 3 s, the time is in the transfer or before the request: check the size, the phases before it, and the number of requests.

Second split, server side: is it the request path or the dependencies? The server’s own timing — handler duration in its access log, or a trace — separates "we spent 2.4 s in the database" from "we spent 2.4 s in the queue before a thread picked it up" (pool wait, loop lag, accept-queue depth). The first leads to EXPLAIN ANALYZE and query counts; the second to top, run-queue length, and the The Thread Pool Server and The Event-Driven Server metrics. A server that is idle and still slow is either waiting on something remote (database, cache, a third-party API — measure each) or blocked on disk (iostat).

Second split, client side: is it round trips or bytes? Count them. The devtools waterfall shows how many connections were opened (each one is DNS + TCP + TLS unless reused — Keep-Alive and Connection Reuse, HTTP/2: Streams on One Connection multiplexing, Connection Pooling) and how deep the dependency chain is (a page that fetches a script that fetches a config that fetches data pays 4 sequential round trips before rendering). Bytes: log2(size/14 kB) round trips to ramp the congestion window means a 2 MB uncompressed bundle from 100 ms away is ~700 ms of pure cwnd growth before bandwidth matters at all. A CDN moves the RTT for static assets from 100 ms to 15 ms; nothing else moves it.

Each split closes half the matrix. Three or four splits reach a row, and the row’s lesson explains the fix. The discipline is to measure the boundary before naming the layer — the failure of most three-second investigations is that each team measures inside its own layer, finds it healthy, and stops.

The first split, from the user’s region (values illustrative)
$ curl -o /dev/null -s -w 'dns %{time_namelookup}  connect %{time_connect}  tls %{time_appconnect}  ttfb %{time_starttransfer}  total %{time_total}  size %{size_download}\n' https://app.example.com/
dns 0.212  connect 0.318  tls 0.431  ttfb 2.905  total 3.012  size 48213

# ttfb − tls = 2.47 s: the server (or its dependencies) owns the time.
# dns 212 ms: uncached from Warsaw — a second, smaller problem (TTL? resolver?).
# 48 kB body in 107 ms: transfer is not the issue this time.

What a strong answer sounds like

Conceptual

A strong answer to the interview question (A user in Warsaw waits 3 seconds. Where did the time go?) names the layers in path order, gives an order of magnitude for each, states that 3 s cannot be explained by RTT alone on a healthy path from a fast server, and then proposes the bisection: curl -w from Warsaw first, then server-side timing, then either the database or the client waterfall. It mentions the things that silently multiply RTT — uncached DNS, no keep-alive, TLS 1.2, N+1 queries, sequential dependent fetches, a CDN miss, a cross-region database — because those are how 500 ms becomes 3 s without anything being "broken".

It also knows what each domain contributes and does not pretend one explains everything. The OS explains why an idle-looking server can still queue (scheduling, pool, loop lag, disk). Networking explains the round trips and the ramp. Database explains the query and the buffer cache. System Design explains the CDN, the balancer and the region choice. DSA explains why the handler took a second. And the answer ends with a number: "I would expect ~600 ms for this page from Warsaw; the missing 2.4 s is on the server, and I would find it with a trace".

The Capstone: A Server With 50,000 Concurrent Connections and Capstone: What Happens When You Visit https://example.com each ask the single-domain version of this question; Where the Time Goes: The Request Timeline and Latency: Same Machine to Cross-Continent give the networking numbers; Follow send() Through the OS to recv() gives the OS half of every network row. This lesson is the map that joins them.

  • Budget first, then bisect; measure boundaries, not layers.
  • Round trips multiply: count connections and dependency depth before optimising anything.
  • A server’s "think time" includes queueing before the handler runs; an idle CPU does not clear the OS.
  • End with an expected number and where the difference is.

Key points

  • A page from 100 ms away needs ~5–8 RTTs minimum (DNS, TCP, TLS, request, cwnd ramp): 500–800 ms with a fast server. Three seconds is extra round trips, server time, loss, or dependent fetches.
  • Build the budget: every layer in path order, its domain, an order-of-magnitude cost, and the one measurement that isolates it.
  • Bisect at boundaries: curl -w splits network from server; server timing splits queueing from dependencies; the client waterfall splits round trips from bytes.
  • Silent multipliers: uncached DNS, no keep-alive, TLS 1.2, CDN miss, per-request upstream connections, N+1 queries, cross-region database, sequential dependent resources, uncompressed bundles.
  • The OS contributes queueing that an idle CPU does not reveal: accept backlog, pool queue, event-loop lag, disk waits, scheduler latency under saturation.
  • Each row belongs to a domain and no domain explains the whole; the capstone is the map.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why start with curl -w rather than the server logs?

Because it measures from the user’s side and separates the phases the server cannot see — DNS, connect, TLS, transfer — from the one it can. One command closes half the matrix; the server logs cannot tell you the DNS lookup took 200 ms.

Why does distance matter so much more than bandwidth for a page load?

Because a page is dozens of round trips, not one transfer. Each handshake, each dependent fetch, and each doubling of the congestion window costs an RTT; bandwidth only matters once the window is large. A 100 Mbit/s link 100 ms away loads a typical page slower than a 10 Mbit/s link 10 ms away.

Why can a server with 5% CPU add two seconds?

Because time is spent waiting, not computing: in the accept queue, in the pool queue, behind a blocked event loop, on a lock, on a database round trip, on disk. All of those are invisible to CPU utilisation and visible to queue and wait metrics.

Capstone: the Warsaw latency budget

Capstone: why does a request from Warsaw take 3 seconds?
Sixteen places where time goes. Edit the numbers, name the Engineer Atlas domain that explains each one, and find the biggest cut.
OSNetworkingDatabaseSoftware ArchitectureSystem DesignDSAAgentictotal 3.05 s — target: 3,000 ms explained
AreamsDomain that explains itFeedback
DNS
Connection establishment
TLS
Network RTT
Packet loss
CDN
Load balancer
Server scheduling
Application CPU
Thread / event loop
Database network call
Database query
Disk I/O
Response size
Congestion
Browser processing

Contributions are seeded, editable, and illustrative — 2.0 MB of JSON over a 150 ms RTT really does cost this much, but your numbers will differ.

0/0 domains right · total 3.05 sSimulated

How it fails

What the failure looks like from inside real software.

  • Each team measures inside its own layer, finds it healthy, and the incident is closed as "the network" — the classic outcome the bisection exists to prevent.
  • Optimising bandwidth (a bigger instance, a faster link) when the page is round-trip-bound; the RTT count is unchanged and so is the load time.
  • A CDN in front of a page whose HTML is dynamic and uncacheable: static assets get fast, the base document still crosses the ocean, and the waterfall barely moves.
  • Connection pooling to the database disabled by a config change: each request pays a TCP + TLS handshake to the database — 2 RTTs — invisible in the query time.
  • Reasoning from a load test in the same region as the server: 40 ms total, "it is fast"; the user in Warsaw still waits 3 s because the test never paid the RTTs.
  • A 200 ms uncached DNS lookup on every page view because the record’s TTL is 30 s; nobody measured time_namelookup.