Skip to main content

Modern HTTP (2 & 3)

HTTP/1.1 was simple but slow for pages made of many small objects. HTTP/2 and HTTP/3 keep the same methods, headers, and status codes - the same semantics - but change how requests travel over the wire.

Pick a version and watch the same four requests schedule on a timeline:

request
Transport: TCPOne request at a time per connection - head-of-line blocking.
req 1
req 2
req 3
req 4
012345678
4 requests complete in 8 ticks - each request waits for the previous one.

HTTP/1.1: one at a time

On a persistent HTTP/1.1 connection, requests are sent and answered in order. A slow or large response stalls everything queued behind it. This is head-of-line (HoL) blocking. Browsers worked around it by opening several parallel TCP connections per site, but that wastes resources and each connection still blocks internally.

HTTP/2: multiplexed streams

HTTP/2 introduces streams: many independent request/response exchanges share one TCP connection, and their data is interleaved as small frames. Now a slow response no longer blocks the others at the HTTP layer - they all make progress over the single connection.

But there is a catch. Everything still rides on one TCP connection, and TCP delivers bytes strictly in order. If one packet is lost, TCP holds back all streams until it is retransmitted - HoL blocking has moved down into the transport layer.

HTTP/3: QUIC over UDP

HTTP/3 fixes that by dropping TCP. It runs over QUIC, a transport built on UDP that provides its own streams with independent delivery. A lost packet stalls only the stream it belonged to; the others keep flowing. QUIC also folds the transport and encryption handshakes together, so connections set up faster.

VersionTransportConcurrencyHoL blocking
HTTP/1.1TCPOne request per connectionAt the HTTP layer
HTTP/2TCPMultiplexed streams, one connectionMoved to TCP
HTTP/3QUIC / UDPIndependent streamsEliminated

Worked example: loading a page with 30 objects

Say a page needs 30 small objects (CSS, JS, icons) from one origin, and the RTT to that server is 80 ms. Browsers cap HTTP/1.1 at roughly 6 parallel connections per origin, so the 30 requests queue up in 30 / 6 = 5 waves, each wave costing one RTT: 5 * 80 ms = 400 ms of pure round-trip time before the last object even starts downloading, on top of a first connection-setup RTT.

HTTP/2 multiplexes all 30 requests over a single connection, so they all go out together after one setup RTT: roughly 80 ms (plus one RTT for the TCP handshake, and another for TLS if it is not resumed) instead of 400 ms+. The gap only grows with more objects or a longer RTT - it is exactly why HTTP/2 felt so much faster on mobile networks, where 80-150 ms RTTs were common.

Gotcha

HTTP/2's fix is incomplete in practice. Because every stream still rides the same TCP connection, a single lost packet stalls all 30 multiplexed streams until TCP retransmits it - the classic "HTTP/2 is slower than HTTP/1.1 on a lossy Wi-Fi network" complaint. Teams that saw this in production either fell back to HTTP/1.1-style connection sharding for flaky clients, or moved to HTTP/3/QUIC, whose per-stream loss recovery does not have this problem.

info

The application-visible model - methods, headers, status codes - is identical across all three. What changes is the plumbing underneath, and that plumbing is where the latency wins come from.