Why three versions
HTTP is an application-layer protocol on top of [[tcp-handshake|TCP]] (or [[udp-basics|UDP]] in HTTP/3). It has been revised twice to address accumulated performance problems. The versions are negotiated at connection time through [[tls-handshake|TLS]] ALPN: client and server agree on a version during the handshake.
HTTP/1.1 (1997)
Text-based request/response. Each exchange is a single request and response separated by CRLF delimiters:
GET /api/users HTTP/1.1
Host: example.com
User-Agent: curl/8.0
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 42
{"users":[{"id":1,"name":"alice"}]}Key features:
- Host header - one IP, many virtual hosts
- Keep-alive - reuse a TCP connection for multiple requests (on by default)
- Chunked transfer encoding - stream a response without knowing Content-Length in advance
- Pipelining - send requests without waiting for responses (disabled in practice due to HoL blocking, see below)
Main pain point with HTTP/1.1:
- One request at a time per connection (L7 head-of-line blocking)
- Browsers open 6+ TCP connections to the same host to download in parallel
- Headers repeat in every request (cookies, user-agent, ...)
HTTP/2 (2015): binary multiplexing
The same GET /api/users request works, but the format is binary:
- One TCP connection carries many streams (logical channels)
- Streams are multiplexed: frames from different requests interleave
- HPACK compresses headers; repeated cookies and tokens are sent as an index, not a full string
- Server Push lets the server send resources before the client requests them (deprecated in practice, rarely useful)
- Stream priority lets you tell the server "this CSS matters more than that image"
In practice: one TCP socket per host instead of six, headers compressed, parallel requests do not block each other at L7.
However, if a TCP packet is lost, all streams on that connection stall until retransmission completes. That is TCP head-of-line blocking. HTTP/2 does not solve it; it only makes it more visible.
HTTP/3 (2022): QUIC replaces TCP
Same binary semantics as HTTP/2, but the transport is QUIC over UDP:
- Each stream is independent: a lost packet in one stream does not block the others (no TCP HoL)
- Handshake takes 1 RTT (or 0 RTT for resumed connections) because TLS is built into QUIC
- Connection migration: switching networks (Wi-Fi to 4G) does not break the connection; QUIC binds to a Connection ID, not to the 5-tuple
- Encryption is mandatory
Drawbacks of HTTP/3:
- UDP is often rate-limited by firewalls and load balancers
- Harder to observe (no native netstat equivalent for QUIC until recently)
- Requires ALPN
h3over TLS 1.3
Comparison
| Property | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Transport | TCP | TCP | QUIC/UDP |
| Format | text | binary | binary |
| Multiplexing | no | yes | yes |
| Head-of-line blocking | L4 + L7 | L4 (TCP) | none |
| Header compression | no | HPACK | QPACK |
| TLS | optional | optional | required |
| Server Push | no | yes* | yes* |
*deprecated, not used in practice
What you see in tcpdump
# HTTP/1.1: plaintext visible
tcpdump -i any -nn -A 'tcp port 80'
# HTTP/2: binary, but ALPN marker 'h2' is visible in ClientHello
tcpdump -i any -nn 'tcp port 443' -X
# HTTP/3: UDP on port 443
tcpdump -i any -nn 'udp port 443'
Which version to use in 2026
- CDN / edge - HTTP/3 for users on Wi-Fi, HTTP/2 as fallback
- Internal API - HTTP/2 (one long-lived connection, gRPC runs over it)
- Legacy / simplicity - HTTP/1.1, parseable from any language in two lines of code
When things go wrong
- HTTP/2 RST_STREAM rapid reset - DDoS vector from 2023; patched in nginx/envoy
- QUIC blocked by firewall - falls back to HTTP/2 via Alt-Svc
- HPACK bomb - malicious Huffman-encoded headers via compression; keep your server up to date
- Header size limit - HTTP/2 defaults to 16 KB of headers; a large request will fail unless you raise the limit