HTTP/1.1 vs HTTP/2 vs HTTP/3
Published
The meaning of HTTP has barely changed since the 1990s: a method, a path, headers, a body, a
status code. GET /cart means the same thing in all three versions. What changed is how
those messages travel, and each version exists to fix the bottleneck the previous one hit.
- HTTP/1.1 (1997): text over TCP, one request at a time per connection.
- HTTP/2 (2015): binary frames, many requests at once over one TCP connection.
- HTTP/3 (2022): the same idea as HTTP/2, but over QUIC instead of TCP.
HTTP/2 lets all of a page's requests share one connection at the same time. On a train with a patchy 4G signal, some users found HTTP/2 slower than HTTP/1.1 with six connections. How could one shared connection be worse than six separate ones?
HTTP/1.1: one at a time#
A request is plain text: GET /style.css HTTP/1.1, some headers, a blank line. On one TCP
connection, the client sends a request and waits for the full response before sending the
next. (Pipelining, sending several without waiting, is in the spec but was never usable in
practice: responses still had to come back in order, and broken proxies mangled it.)
A modern page makes 70 or more requests. One at a time would be painfully slow, so browsers
open up to six connections per host and developers invented workarounds that you can
still find in old codebases: domain sharding (spread assets over img1., img2. hosts to
get more connections), sprite sheets (many icons in one image), and concatenating
every script into one giant bundle. Each new connection also costs its own TCP and TLS
handshake. Headers are repeated in full, as text, on every request: cookies and user agents
can be a kilobyte or more per request.
HTTP/2: many at once, one connection#
HTTP/2 keeps the meaning and changes the wire format. Messages are split into binary frames, each tagged with a stream id, and frames from many streams interleave on a single TCP connection. Seventy requests can be in flight at once with one handshake. Headers are compressed with HPACK, which remembers headers already sent on the connection, so the repeated cookie costs a few bytes the second time.
The sharding and sprite hacks become pointless and even harmful (sharding defeats the single
connection). Server push, the feature that let a server send files before they were asked
for, turned out to waste bandwidth more often than it helped; Chrome removed it in 2022.
103 Early Hints does the job better.
The catch is underneath. HTTP/2 multiplexed the streams, but TCP still delivers one ordered byte stream. If one packet is lost, every frame behind it waits, whichever stream it belongs to. On a clean network that rarely matters. On a lossy one, all requests now share one fate, where with HTTP/1.1 only one of six connections stalled. That is the answer to the question above.
HTTP/3: the same idea over QUIC#
HTTP/3 maps HTTP/2's streams onto QUIC streams (see TCP vs UDP vs QUIC). QUIC orders each stream on its own, so a lost packet only stalls its own stream. You also get QUIC's one round trip handshake with TLS built in, and connections that survive a switch from WiFi to mobile data. Header compression is redone as QPACK, because HPACK assumed in order delivery.
Try it: three files, one lost packet, three protocols.
Side by side#
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Format | Text | Binary frames | Binary frames |
| Requests per connection at once | 1 | Many (streams) | Many (streams) |
| Connections per site | Up to 6 | 1 | 1 |
| Header compression | None | HPACK | QPACK |
| Head of line blocking | At HTTP level | At TCP level | None between streams |
| New connection handshake (with TLS 1.3) | 2 RTT | 2 RTT | 1 RTT, 0 on resume |
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| HTTP/1.1 | Simple, readable with telnet, supported by every client and proxy ever made. | One request at a time per connection, repeated headers, several handshakes. | Simple internal calls, legacy clients. Your server should still speak it as a fallback. |
| HTTP/2 | Multiplexing and header compression over one connection. Required by gRPC. | TCP head of line blocking on lossy networks. Long lived connections complicate load balancing. | The default between browsers and servers, and for service to service traffic. |
| HTTP/3 | No cross stream blocking, faster handshakes, survives network changes. | UDP is blocked on some networks, needs a TCP fallback, more CPU, less mature debugging tools. | Public traffic to mobile users and long distance links. Usually switched on at the CDN. |
Clients discover HTTP/3 through an Alt-Svc: h3=":443" header on an HTTP/2 response (or a
DNS HTTPS record), then try QUIC on the next connection and quietly fall back to TCP if UDP
is blocked. In practice you rarely run HTTP/3 yourself: Cloudflare, Fastly, Akamai and the
big cloud load balancers speak it at the edge and use HTTP/1.1 or HTTP/2 back to your servers.
In the interview#
Checkpoint#
1. Why can HTTP/2 be slower than HTTP/1.1 on a lossy network?
2. What does HTTP/3 change compared with HTTP/2?
3. Which HTTP/1.1 era optimisation becomes harmful under HTTP/2?
- 1.1: one request at a time per connection, 6 connections per host.
- 2: binary frames, multiplexed streams, HPACK, one TCP connection. TCP head of line blocking remains.
- 3: HTTP over QUIC, QPACK, per stream ordering, 1-RTT handshake.
- Same semantics in all three. Only the wire changed.
- Long lived HTTP/2 connections need layer 7 load balancing.
All three versions carry the same HTTP semantics; what changes is the wire. HTTP/1.1 is text over TCP with one request in flight per connection, so browsers open six connections per host and we used sharding, sprites and bundling to cope. HTTP/2 splits messages into binary frames on numbered streams, multiplexes many requests over one TCP connection and compresses headers with HPACK, which removes the HTTP level head of line blocking. But TCP still delivers one ordered stream, so a lost packet stalls every request, which can make HTTP/2 worse on lossy networks. HTTP/3 runs the same model over QUIC, which orders each stream independently, handshakes in one round trip with TLS built in, and survives network changes. In practice I enable HTTP/3 at the CDN with a TCP fallback, use HTTP/2 internally, and use layer 7 balancing because HTTP/2 connections are long lived.