Concepts / Network services

Network services

HTTP/2

Also called HTTP versions, HTTP/1.1, HTTP/1.0, h2, multiplexing, stream, frame, HPACK, head-of-line blocking

HTTP's meaning in binary frames: many requests as streams over one TCP connection, headers compressed.

Methods, paths, status codes and headers are unchanged; the request line becomes pseudo-headers (:method, :path, :authority) and everything travels in frames (SETTINGS, HEADERS, DATA) tagged with a stream ID. Streams interleave on one connection (multiplexing), so a page no longer needs six connections, and HPACK turns repeated headers into table indexes. All streams still share one TCP byte stream: a lost segment stalls them all (TCP head-of-line blocking). Browsers speak it only over TLS, chosen by ALPN.

How HTTP versions differ, step by step

The web has spoken four versions of HTTP, each keeping the meaning (methods, paths, status codes, headers) and changing how the bytes travel. Follow them on the HTTP versions lab, where laptop fetches from webServer.lab (Caddy on port 443, at 10.0.2.10):

  1. HTTP/1.0 (1996). One request per TCP connection, then the connection closes. A page with many files meant a new handshake for each.
  2. HTTP/1.1 (RFC 9112, 1997). Keep-alive holds the connection open for several requests, but still one at a time: the next request waits for the previous response (head-of-line blocking at the message level). Browsers work around it by opening about six connections per origin.
  3. HTTP/2 (RFC 9113, 2015). The same request becomes binary frames on numbered streams. The request line turns into pseudo-headers (:method, :path, :authority, :scheme); a HEADERS frame opens a stream (odd IDs from the client), DATA frames carry the body. Many streams interleave on one TCP connection (multiplexing), and HPACK compresses repeated headers. One lost TCP segment still stalls every stream: TCP head-of-line blocking.
  4. HTTP/3 (RFC 9114, 2022). The same streams move onto QUIC over UDP port 443, with TLS 1.3 built in. QUIC's streams are independent, so a lost packet stalls only its own stream.

Browsers choose the version over TLS with ALPN: the ClientHello offers h2, http/1.1, and the server picks. HTTP/3 is found afterwards through the alt-svc header, covered in HTTP/3 and QUIC.

What it looks like on a Linux machine

curl -v shows ALPN choosing the version. Over HTTP/1.1 the status line is plain text:

$ curl -sv --http1.1 -o /dev/null https://webServer.lab/
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
< HTTP/1.1 200 OK

Over HTTP/2 the Host header becomes the :authority pseudo-header, the status line loses its reason phrase, and the server advertises HTTP/3:

$ curl -sv --http2 -o /dev/null https://webServer.lab/
* ALPN: server accepted h2
< HTTP/2 200
< alt-svc: h3=":443"

nghttp -nv shows the frames: each side opens with SETTINGS, the request is a HEADERS frame that opens a stream, the body comes back as DATA frames on that stream.

$ nghttp -nv https://webServer.lab/
send SETTINGS frame <length=..., stream_id=0>
send HEADERS frame <length=..., flags=0x25, stream_id=13>
recv DATA frame <length=..., flags=0x00, stream_id=13>

HPACK's saving is visible with h2load -n 20 -c 1 https://webServer.lab/: twenty requests on one connection. It prints Application protocol: h2, and its traffic: line ends with headers (space savings N%). After the first request, a header that repeats is sent as a small index into a shared table, so the saving is well over 80%.

With --http3-only, curl reports using HTTP/3 and HTTP/3 200, and a capture shows only UDP to port 443.

HTTP/1.0, 1.1, 2 and 3 compared

All four carry the same methods, headers and status codes; what changes is the transport underneath.

Version Transport Requests per connection Header handling Lost-packet effect
HTTP/1.0 TCP, one per connection 1, then close plain text, resent each time n/a
HTTP/1.1 TCP, keep-alive serial, one at a time plain text, resent each time stalls the one message
HTTP/2 one TCP connection many streams, multiplexed HPACK compression stalls every stream (TCP HOL)
HTTP/3 QUIC over UDP 443 many independent streams QPACK compression stalls only its own stream

HTTP/2 fixed the message-level blocking of HTTP/1.1 but left the transport-level blocking of TCP; HTTP/3 moved to QUIC to remove that too.

What goes wrong, and how you see it

HTTP/2 falls back to HTTP/1.1. If the server offers no h2 in ALPN, curl -v shows ALPN: server accepted http/1.1 even with --http2. HTTP/2 over TLS needs ALPN to agree; prior knowledge (cleartext h2c) is rarely used.

HTTP/3 never connects. curl --http3-only hangs or fails if UDP 443 is blocked. Firewalls that pass only TCP drop QUIC; the browser silently stays on HTTP/2. Check that UDP 443 is open both ways.

Alt-Svc is missing, so HTTP/3 is never discovered. Without alt-svc: h3=":443" in a response, a client that reached the site over TCP has no reason to try QUIC. curl -sI should show the header.

Head-of-line blocking still bites on HTTP/2. On a lossy path one dropped segment stalls all streams. The symptom is every request on the connection pausing together; HTTP/3 is the cure because QUIC streams are independent.

Questions people ask

Is HTTP/2 faster than HTTP/1.1? Usually, because one connection carries many multiplexed streams instead of six connections each doing one request at a time, and HPACK shrinks headers. On a lossy link TCP head-of-line blocking can erase the gain, which is what HTTP/3 addresses.

Does HTTP/2 require HTTPS? The spec allows cleartext h2c, but every browser speaks HTTP/2 only over TLS, chosen by ALPN. In practice HTTP/2 means HTTPS.

HTTP/2 vs HTTP/3? Both multiplex streams; HTTP/2 runs on TCP, HTTP/3 on QUIC over UDP so a lost packet stalls only one stream. See HTTP/3 and QUIC.

See it on real machines

Learn it on real machines.

Every lab is a small network of real Linux machines in your browser. Start with a free account: Google or email, no card.

Open this lab