Library
6 min read

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.
Your answer

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.

Load a page, lose one packet
On the wire, in send order
H1C1I1H2C2I2H3C3I3H4C4I4
HTML160 ms
CSS160 ms
Image160 ms
First paint (HTML + CSS): 160 msEverything: 160 msStriped: extra time caused by the lost packet
One image packet was lost. TCP will not hand anything after the gap to the browser until it is resent, so the HTML and CSS, which arrived intact, sit in a buffer. First paint slips from 140 ms to 160 ms because of a picture. That is head of line blocking at the TCP layer.
Ticks of 10 ms. A request takes 30 ms to reach the server, and a lost packet takes 100 ms to be noticed and resent. Real numbers vary; the shapes do not.

Side by side#

HTTP/1.1HTTP/2HTTP/3
TransportTCPTCPQUIC over UDP
FormatTextBinary framesBinary frames
Requests per connection at once1Many (streams)Many (streams)
Connections per siteUp to 611
Header compressionNoneHPACKQPACK
Head of line blockingAt HTTP levelAt TCP levelNone between streams
New connection handshake (with TLS 1.3)2 RTT2 RTT1 RTT, 0 on resume
ChoiceWhat you gainWhat you payPick it when
HTTP/1.1Simple, 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/2Multiplexing 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/3No 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#

Interviewer
What problem does HTTP/2 solve, and what problem does it leave?
A two part question. Most people only answer the first.
You
It solves HTTP-level head of line blocking: in HTTP/1.1 a connection carries one request at a time, so browsers open six connections and we hacked around it with sharding and bundling. HTTP/2 multiplexes many streams over one connection and compresses headers. What it leaves is TCP-level head of line blocking: TCP delivers one ordered byte stream, so a lost packet stalls every stream on the connection. HTTP/3 fixes that by running over QUIC, which orders each stream independently.
Two kinds of head of line blocking, at two layers. That distinction is the whole answer.
Interviewer
Does HTTP/2 affect how you load balance?
You
Yes. Clients keep one long lived connection, so a layer 4 load balancer pins all of a client's requests to one backend. For gRPC and other HTTP/2 traffic between services I would use a layer 7 balancer that spreads individual requests, or client side balancing.
Still worth doing on HTTP/2?
Domain sharding across img1, img2, img3
Compressing responses with Brotli or gzip
Sprite sheets for every icon
Using a CDN

Checkpoint#

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?

Worth memorising
  • 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.
Say this in 60 seconds

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.