Network services
HTTP/2
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):
- HTTP/1.0 (1996). One request per TCP connection, then the connection closes. A page with many files meant a new handshake for each.
- 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.
- 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. - 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.