Module F-5·18 min read

DNS → TCP handshake → TLS → HTTP, hop by hop, with the milliseconds attributed — so you know which layer to blame before you start guessing.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module F-5 — The Anatomy of a Request

What this module covers: A request took 800ms and the server-side trace says the handler ran for 40ms. Where did the other 760ms go? This module accounts for every hop: DNS resolution and the caching layers that usually hide it, the TCP three-way handshake, TCP slow start and why the first 14 KB is special, TLS 1.2 vs 1.3 handshake costs and session resumption, then the server-side path and the response. It ends with the cold-connection vs warm-connection budget side by side — because connection reuse is the single largest and cheapest latency win available, and understanding why is what makes Modules F-6, F-10, and P-4 make sense.


The 760 Milliseconds Nobody Owns

Here's an incident pattern that repeats in every organisation. A user in Bengaluru reports that an API call takes about a second. The APM dashboard shows the handler executing in 40ms, p99 at 60ms. Both observations are correct, and the gap between them is entirely made of work that happens before your code runs and after it returns.

Most of that gap is round trips. And a round trip is not a thing you can optimise away with better code — it's a fixed toll set by distance and by how many times the protocols insist on going back and forth before any useful bytes move.

Analogy: a request is a courier delivery, and your application code is only the moment the package is handed over. Before that: looking up the address (DNS), the courier driving to the building (TCP handshake), showing ID at reception and getting a badge (TLS), and being escorted to the right floor (routing). Optimising the handover from 40 seconds to 20 seconds is irrelevant if the drive takes an hour. Almost all real latency work is about the drive.

Why this matters in production: if you can't attribute latency to a hop, you'll optimise the wrong one. Teams routinely spend a sprint shaving 15ms off a query while every request pays 300ms for a TLS handshake it shouldn't be doing at all.


Step 1: DNS — Turning a Name Into an Address

Before a single byte reaches your server, the client needs an IP address. Full recursive resolution walks a hierarchy:

text

A completely cold recursive lookup can cost 20–120ms, sometimes more with a distant or overloaded resolver. A warm one is effectively free. In practice the great majority are warm, which is exactly why DNS is invisible until the day it isn't.

Three things about DNS that show up in real designs:

TTL is a promise nobody fully keeps. You set a 60-second TTL expecting fast failover; some resolvers and some client runtimes cache longer, and long-lived processes may resolve once at startup and never again. So DNS-based failover has a real tail — a fraction of clients keep hitting the dead IP for minutes. This is why serious failover uses a load balancer with a stable address, or anycast, rather than swapping A records and hoping (Modules F-11, A-9).

Every distinct hostname is a separate resolution. A page pulling from your API, a CDN, an analytics vendor, a font host, and a payment iframe pays DNS separately for each. This is why dns-prefetch and preconnect hints exist, and why consolidating third-party origins is a measurable front-end win.

Server-side runtimes often don't cache DNS the way you assume. Node.js delegates to the OS resolver with no in-process caching by default, so a service making outbound calls to a hostname can resolve on every connection. At high request rates this becomes visible — both as latency and as load on the resolver. The fix is an explicit caching resolver or connection reuse that makes the question moot.


Step 2: TCP — One Round Trip Before You Say Anything

TCP requires a three-way handshake to establish a connection:

text

That's one full round trip (1 RTT) before any application data flows. On a same-city connection that's ~5–20ms. Mumbai to Virginia, it's ~180ms — gone, before your server has heard what the client wants.

Then comes the part fewer people know about, and it matters more than the handshake for small responses: TCP slow start.

A new connection doesn't know how much bandwidth is available, so it starts conservatively and doubles each round trip. The initial congestion window on modern Linux is 10 packets — roughly 14 KB of payload. The consequences are sharp:

Response sizeRound trips to deliver (new connection)
≤ 14 KB1
≤ ~42 KB2
≤ ~100 KB3
≤ ~220 KB4

So a 90 KB JSON response over a fresh connection to a server 180ms away costs roughly 3 RTTs of transfer — ~540ms — not because bandwidth is short, but because the connection hasn't earned permission to send faster yet. Bandwidth is nearly irrelevant here; round trips are everything.

Two design consequences worth remembering:

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.