Skip to content

Networking and the web

What actually happens between a browser and your server, and where the time goes.

01

The life of a request

beginner

Typing a URL sets off DNS resolution, a TCP handshake, a TLS handshake, and then the HTTP request itself. Naming those four steps separately is most of the skill, because each one costs a round trip and round trips are what you actually pay for.

The prices are known. DNS is one round trip if nothing is cached, and often several as the resolver walks the chain. TCP's three-way handshake is one. TLS 1.3 is one, TLS 1.2 is two, and TLS 1.3 with session resumption can be zero for a returning client. Then the request itself. A user 200ms away pays that 200ms on each of them: four round trips is 800ms of blank screen before a single byte of content moves.

Bandwidth does not help with any of this. The pipe is not full during a handshake, it is idle and waiting, which is why upgrading a connection from 50 to 500 megabits changes page load far less than people expect and why moving the server closer changes it far more. Above a few megabits, latency decides perceived speed and bandwidth decides how long large files take.

It gets worse in sequence. Anything that has to happen in order pays the price again: a redirect from the apex to www is one extra round trip plus, if it crosses to a different host, another DNS lookup and another handshake. A page that fetches a script that then fetches configuration that then fetches data is three sequential trips before anything renders, and the browser cannot start any of them early because it does not know they exist.

Then the connection is reused, which is the part that makes the arithmetic bearable. Keep-alive means subsequent requests on the same connection skip DNS, TCP and TLS entirely, so the first request is expensive and the tenth is nearly free. That is why third-party scripts on other hosts are disproportionately costly, and why domain sharding, once a performance technique, is now the opposite.

The practical conclusion is short. Remove a round trip, move the server closer, or make the trips happen in parallel rather than in sequence. Everything else in web performance is a variation on one of those three.

Check yourself

Why does a redirect from example.com to www.example.com cost more than it looks?

name to addressconnectnegotiatefinally askBrowser200ms awayDNS1 round tripTCP handshake1 round tripTLS 1.31 round tripHTTP request1 round trip
ClientEdge / CDNServiceDashed = asynchronous
Four round trips before the first byte of content
Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

02

TCP and UDP

beginner

TCP guarantees delivery and order: lost packets are retransmitted, and the receiver holds everything after the gap until it is filled. UDP does neither. It sends a datagram and forgets it, and anything that arrives, arrives. That guarantee is not free, and the price is head-of-line blocking. One lost packet stalls everything queued behind it, which is exactly right for a file, where byte 400 is meaningless without byte 399, and useless for a live call, where a frame retransmitted 400ms later is worthless by the time it lands. Video calls, games and DNS run on UDP and handle loss themselves; nearly everything else takes the guarantee and is glad of it.

TCP also does congestion control, which is the part people forget when they reach for UDP. It probes for available capacity, backs off when it sees loss, and shares the path with other flows in a roughly fair way. Choosing UDP means either implementing that yourself or being the traffic that ruins the network for everyone else, and the internet works partly because most flows are well behaved.

There is a third cost worth knowing: connection setup. TCP's three-way handshake is a full round trip before any data, and TLS adds one or two more on top. UDP has no handshake at all, which is why DNS uses it for a query and a reply that would otherwise cost three exchanges to set up a connection carrying one small message.

QUIC is the interesting middle. It runs on UDP and rebuilds reliability, ordering and congestion control in user space, per stream rather than per connection, so a lost packet stalls only the stream it belonged to. It also folds the transport and TLS handshakes together, so a connection is established in one round trip and a resumed one in zero. That is TCP's guarantees without TCP's blocking, at the cost of being much newer and living in user space where it can be updated.

The rule of thumb is unchanged: take the guarantee unless stale data is worthless. If you find yourself adding sequence numbers, acknowledgements and retransmission on top of UDP, you are reimplementing TCP, and the version you write will be worse than the one in the kernel.

Check yourself

Besides reliability, what does a team give up by choosing raw UDP?

take the guaranteeskip itthe pricethe pointthe hidden billor both, per streamFile transferbyte 400 needs byte399Live calla late frame isworthlessTCPordered,retransmittedUDPno order, no retriesQUICordered per streamHead-of-line stallone loss stalls therestLoss is droppedplayback continuesYou own reliabilityand congestioncontrol
ClientEdge / CDNData storeDashed = asynchronousConsidered, not chosen
The guarantee is worth paying for, unless stale data is worthless
Evidence · 1 source
  1. 01
    RFC 9000: QUIC, a UDP-based multiplexed and secure transport

    That QUIC provides per-stream ordering over UDP, folds in the TLS handshake, and identifies connections by a connection id rather than the address tuple.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

03

HTTP/1.1, HTTP/2 and HTTP/3

intermediate

HTTP/1.1 carries one request at a time per connection. Pipelining is in the specification and is effectively unused, because a slow response blocks the ones behind it and proxies handled it badly, so browsers instead open around six connections per host and developers bundled files to fit more work through them.

That constraint produced an entire generation of workarounds: sprite sheets combining twenty icons into one image, inlined CSS, concatenated JavaScript, and domain sharding, which spread assets across asset1, asset2 and asset3 to get eighteen connections instead of six. Every one of them was a workaround for a protocol limit, and every one of them costs something: a sprite sheet invalidates entirely when one icon changes, and sharding multiplies DNS lookups and handshakes.

HTTP/2 multiplexes many streams over one connection, which removes the reason for all of it. It also compresses headers with HPACK, which matters more than it sounds when a request carries a kilobyte of cookies and the body is a hundred bytes, and it supports server push, which was so hard to use correctly that Chrome removed support for it.

What HTTP/2 does not fix is that it still runs on TCP. TCP delivers one ordered byte stream, so a single lost packet stalls every stream sharing that connection, which is worse than HTTP/1.1's six independent connections on a lossy network. On a good connection HTTP/2 wins comfortably; on a bad mobile link the comparison is closer than the version numbers suggest.

HTTP/3 moves to QUIC over UDP, where each stream is ordered independently, so loss affects one stream instead of all of them. It also merges the transport and TLS handshakes into one round trip, and connections are identified by a connection id rather than by the four-tuple of addresses and ports, so a phone moving from wifi to mobile data keeps its connection instead of starting again.

The practical advice is to stop carrying the workarounds forward. Bundling still helps a little, because compression works better across concatenated files and there is per-request overhead even when multiplexed, but the aggressive version hurts caching: one changed line invalidates a megabyte. Domain sharding is now actively harmful, since it defeats connection reuse and adds handshakes that HTTP/2 was designed to avoid.

Check yourself

Why does HTTP/2 still suffer head-of-line blocking despite multiplexing?

bundle to fitthe workaroundsone connectionstill on TCPone connectionloss isolatedOne page40 resourcesHTTP/1.16 connections perhostHTTP/2multiplexed, HPACKHTTP/3QUIC over UDPSprites, shardingnow actively harmfulOne TCP byte streama loss stalls everystreamIndependent streamsa loss stalls oneWifi to mobile dataconnection idsurvives it
ClientEdge / CDNData storeExternalDashed = asynchronousConsidered, not chosen
Each version removes the previous version's workaround
Evidence · 2 sources
  1. 01
    Chrome Platform Status: HTTP/2 and gQUIC server push removal

    That Chrome removed support for HTTP/2 server push, which is the evidence that a feature nobody could use correctly was not a feature.

  2. 02
    RFC 9114: HTTP/3

    That HTTP/3 runs over QUIC, giving per-stream ordering and a connection identifier independent of the address and port.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

04

DNS and why it hurts you

intermediate

DNS turns a name into an address, and the answer is cached at every layer between you and the client: the browser, the operating system, the corporate resolver, the ISP's resolver. Each holds it for as long as the TTL you published, which makes TTL the single most consequential number in a migration plan. Low TTL means fast changes and more lookups; high TTL means the reverse.

The catch is that your TTL is a request, not an instruction. Some resolvers clamp it to a minimum of their own, some ignore it, and some clients cache for the life of the process regardless. A migration planned around 60 seconds can still be sending traffic to the old address hours later, from a client nobody can identify, which is why the old address has to keep working rather than being decommissioned on schedule.

The folk explanation, that changes take hours to propagate down from the root servers, is wrong and worth unlearning. Nothing propagates: authoritative servers are updated immediately, and the delay is entirely caches holding the previous answer until it expires. That distinction matters because it tells you what to do, which is to lower the TTL well in advance rather than to wait and hope.

The practical technique follows. Days before a cutover, drop the TTL to 60 seconds and let the old, long TTL expire everywhere. Make the change. Keep the old address serving, ideally proxying to the new one, until traffic to it has stopped for longer than any plausible cache. Then raise the TTL again, because a permanently low TTL costs a lookup on every cold client and makes your resolver a hard dependency of every page load.

Better still, do not fail over in DNS at all. Point the name at something stable, a load balancer or an anycast address, and move traffic behind it. Then a failover takes effect in seconds for everyone, rather than in an interval decided by other people's caches. This is why every large system's public name resolves to an address that has not changed in years. One more property worth knowing: negative answers are cached too. If a name does not exist when someone looks it up, that absence is cached according to the SOA record's minimum, so creating a record after a client has already asked for it can leave that client failing for the duration. Create the record before you tell anyone to try it.

Check yourself

Why raise the TTL again after a migration is complete?

hitmissmissmissone name, never changeshours of long tailClientasks onceBrowser cacheown lifetimeOS cacheown lifetimeISP resolvermay clamp your TTLAuthoritativeupdated instantlyStable addressload balancer oranycastDNS cutovercompletes when cachesdo
ClientEdge / CDNData storeServiceDashed = asynchronousConsidered, not chosen
Nothing propagates: caches expire, and they expire when they choose
Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

05

CDNs and edge caching

intermediate

A CDN puts copies of your content in data centres near your users, so the round trips that dominate page load take tens of milliseconds instead of hundreds. Nothing about your origin got faster. The distance got shorter, and distance was the bill.

It helps even when nothing is cached, which surprises people. The connection is established with a nearby edge node, so the expensive handshakes happen over a short path, and the edge holds a warm, tuned connection back to your origin. A cache miss served through an edge is often meaningfully faster than the same miss served directly, purely from terminating TLS close to the user.

Static assets are the easy part, and the reason they are easy is content addressing: a hashed filename can be cached forever because a new build is a new URL. The interesting work is caching HTML and API responses, where you have to be deliberate about the cache key and about invalidation, which are the two things that go wrong and the two things nobody owns.

Cache keys are where hit ratios are won and lost. The key is the URL plus whatever Vary names, so an analytics parameter appended to a shared link produces a distinct entry per recipient, and Vary on a header with high cardinality shatters one entry into thousands. Normalising the key, stripping parameters that do not change the response, is routinely the largest single improvement available and costs nothing to run.

Modern edge platforms also run code at those locations, which changes what can be moved. Authentication checks, personalisation, redirects, A/B assignment and geographic routing can happen near the user rather than at origin, so a personalised page can still be served from a cached shell with the personal part filled in at the edge. That is the difference between a CDN as a file server and a CDN as part of the application.

The failure mode to respect is that an edge cache is very good at holding whatever you told it to hold. A wrong header applied by path rather than by response can pin the wrong content under the right URL for as long as you specified, and no purge reaches copies already held in browsers. Immutable caching is a promise about the URL, and it should only be made where the URL identifies the bytes.

Check yourself

Your API is fast under load tests but slow for real users abroad. What helps most?

handshakes heremost requeststhe restone long trip, reusedUserSydneyEdge node10ms awayCache hitanswered locallyCache misswarm link to originOriginLondon, 250ms
ClientEdge / CDNData storeServiceDashed = asynchronous
The edge shortens the round trips, cached or not
Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.