HTTP versions
In one line
Section titled “In one line”The meaning of HTTP (methods, status codes, headers) has barely changed since 1997. Each new version changed how those messages are carried: HTTP/1.1 reuses a connection, HTTP/2 multiplexes many requests over one TCP connection, HTTP/3 does the same over QUIC on UDP so one lost packet no longer stalls every request.
Why it exists
Section titled “Why it exists”Every version removed the worst delay of the one before.
- HTTP/0.9 (1991). One line:
GET /page.html. The server sends HTML and closes the connection. No headers, no status codes, no other content types. - HTTP/1.0 (RFC 1945, May 1996). Adds a version in the request line, a status line, and headers.
Content-Typemakes images and other files possible. Still one TCP connection per request, so every image pays a new handshake and a new slow start. - HTTP/1.1 (RFC 2068, January 1997; RFC 2616, June 1999; current text RFC 9110 and 9112, June 2022). Connections are persistent by default. The
Hostheader lets many sites share one IP address. Chunked transfer coding lets a server stream a body of unknown length. Pipelining is added and fails in practice. What still hurts: one request at a time per connection. - SPDY (Google) showed that multiplexing over one connection works on the real Internet. It became the base of HTTP/2.
- HTTP/2 (RFC 7540, May 2015; current text RFC 9113, June 2022). Binary frames, many concurrent streams on one connection, header compression. What still hurts: all streams share one TCP byte stream, so one lost packet stalls all of them.
- QUIC (RFC 9000, May 2021) and HTTP/3 (RFC 9114, June 2022). The transport moves to UDP and into user space. Streams are known to the transport, so loss on one does not block the others. The TLS 1.3 handshake is merged with the transport handshake.
Since 2022 the semantics live in one document (RFC 9110) and each version is only a wire mapping: RFC 9112, 9113, 9114.
How it works
Section titled “How it works”HTTP/1.1
Section titled “HTTP/1.1”- Text on the wire. A request line, header lines, an empty line, an optional body. You can type it by hand with
nc. - Keep-alive. The connection stays open after a response. The next request reuses it and skips the TCP and TLS handshakes.
Connection: closeends it. In HTTP/1.0 persistence was an opt-in withConnection: keep-alive. - One request at a time. A connection carries one request, then its complete response, then the next. A slow response blocks everything queued behind it. This is head-of-line (HOL) blocking at the application layer.
- Pipelining. The client sends several requests without waiting. The server must answer in the same order. So a slow first response still blocks the rest, only idempotent requests can be retried safely after a failure, and many proxies handled it wrongly. Browsers do not enable it.
- Parallel connections instead. Browsers open about 6 connections per origin (earlier browsers used 2 to 3). Each one costs a handshake, memory on both sides, and its own congestion window.
- Workarounds of that era: domain sharding (spread assets over
static1,static2to get 6 connections each), image sprites, bundling files. All of them are counterproductive on HTTP/2.
HTTP/2
Section titled “HTTP/2”- Negotiation. Over TLS the client offers
h2in ALPN (Application-Layer Protocol Negotiation) and the server picks it during the handshake, so it costs no extra round trip. Browsers use HTTP/2 only over TLS. Clear text HTTP/2 needs prior knowledge that the server speaks it. The oldUpgrade: h2cmechanism is deprecated in RFC 9113. - Preface. The client sends a fixed 24 byte string that starts with
PRI * HTTP/2.0, then aSETTINGSframe. The server answers with its ownSETTINGS. - Binary framing. Everything is a frame with a 9 byte header: 24 bit length, 8 bit type, 8 bit flags, 31 bit stream identifier. Types include
HEADERS,DATA,SETTINGS,WINDOW_UPDATE,RST_STREAM,PING,GOAWAY. The default maximum frame payload is 16,384 bytes. - Streams. One request and its response are one stream. Client-initiated streams have odd identifiers, server-initiated ones even. Stream 0 is the connection itself.
- Multiplexing. Frames of different streams are interleaved on one TCP connection. A slow response no longer blocks others. One connection per origin is enough.
SETTINGS_MAX_CONCURRENT_STREAMSis unlimited by default and the RFC recommends at least 100. - Flow control. Every stream and the whole connection have a receive window. Both start at 65,535 bytes and grow with
WINDOW_UPDATE. - HPACK (RFC 7541) compresses headers. A static table holds common header fields, a dynamic table (4,096 bytes by default) remembers headers already sent on this connection, and strings are Huffman coded. A repeated
cookieoruser-agentbecomes one or two bytes. - Cancel without closing.
RST_STREAMends one stream.GOAWAYtells the peer to stop opening streams, which is how a server drains gracefully. - Server push let a server send responses nobody asked for. It rarely helped. Chrome disabled it by default in version 106. Use
103 Early Hintsor preload instead. - Priorities. The stream dependency tree of RFC 7540 is deprecated. The replacement is the
Priorityheader of RFC 9218, shared with HTTP/3.
The problem that remains: TCP head-of-line blocking. TCP guarantees one ordered byte stream and knows nothing about HTTP/2 streams. If one packet is lost, the kernel holds back every byte after it until the retransmission arrives, even bytes that belong to other streams. On a lossy link a single HTTP/2 connection can be worse than six HTTP/1.1 connections, because six connections lose independently. RFC 9113 says it plainly: TCP head-of-line blocking is not addressed by this protocol.
HTTP/3 over QUIC
Section titled “HTTP/3 over QUIC”- UDP. QUIC runs over UDP, usually port 443. Reliability, ordering, congestion control and loss recovery are implemented by QUIC itself, in the application or a library, not in the kernel.
- Encryption is built in. QUIC always uses TLS 1.3. The TLS handshake messages travel inside QUIC packets. Almost everything is encrypted, including most of the packet header, so middleboxes cannot read or rewrite transport state.
- 1-RTT setup. One round trip sets up transport and keys together. TCP plus TLS 1.3 needs two. With a ticket from an earlier visit the client can send a request in its first flight (0-RTT). As with TLS, early data can be replayed, so it is only for requests that are safe to repeat.
- Streams in the transport. QUIC delivers each stream in order on its own. A lost packet delays only the streams whose data was in it. HTTP/3 puts each request on one bidirectional stream and uses unidirectional streams for control data.
- Connection IDs and migration. A TCP connection is identified by source and destination IP and port. Change network and it dies. A QUIC connection is identified by connection IDs chosen by the endpoints. When a phone moves from Wi-Fi to cellular, the client sends from the new address, the server checks the new path with
PATH_CHALLENGEandPATH_RESPONSE, and the connection continues. Only the client can migrate, only after the handshake is confirmed, and it uses a fresh connection ID on the new path so observers cannot link the two. - QPACK (RFC 9204) replaces HPACK. HPACK assumes all header blocks arrive in one total order, which would bring head-of-line blocking back. QPACK sends dynamic table updates on a dedicated encoder stream and acknowledgements on a decoder stream. Its static table has 99 entries. The dynamic table capacity and the number of streams allowed to wait for table updates both default to 0, so by default no stream can block on another.
- Protection against abuse. A client’s first datagram must be padded to at least 1,200 bytes, and until the server has validated the client’s address it may send at most 3 times the bytes it received. This limits reflection attacks with spoofed source addresses.
How a client discovers HTTP/3
Section titled “How a client discovers HTTP/3”A URL does not say which version to use, and a client cannot know that UDP 443 works. Two mechanisms:
-
Alt-Svc header (RFC 7838). The first visit goes over TCP with HTTP/2 or HTTP/1.1. The response carries:
alt-svc: h3=":443"; ma=86400This says the same origin is available over HTTP/3 on UDP port 443, and the hint is valid for 86,400 seconds. Without
mathe default is 24 hours. The client remembers it and tries QUIC for later requests. The QUIC endpoint must present a certificate valid for the original host name. Clients drop cached alternatives on a network change unless the header sayspersist=1. -
HTTPS DNS record (RFC 9460). The answer
example.com. HTTPS 1 . alpn="h3,h2"tells the client before any connection that HTTP/3 is available, so even the first request can use QUIC. See DNS.
If QUIC does not connect, the client falls back to TCP. Clients usually race both so a blocked UDP path costs little. curl, for example, starts the TCP attempt after 100 ms without a reply on QUIC and after 200 ms in any case.
Where HTTP/3 helps, and where it does not
Section titled “Where HTTP/3 helps, and where it does not”Helps most:
- Lossy networks. Mobile, congested Wi-Fi, long international paths. No cross-stream stall on loss.
- High latency paths. One round trip less on every new connection, zero with 0-RTT.
- Clients that change networks. Connection migration keeps downloads and calls alive across a Wi-Fi to cellular switch.
- Pages with many small resources from one origin.
Helps little or not at all:
- Clean, low latency links, such as inside a data centre. With no loss there is no TCP head-of-line blocking to remove, and connections are long-lived so the handshake saving is rare.
- One large download. A single stream has nothing to be blocked by. Throughput then depends on congestion control and CPU cost.
- Service to service traffic. gRPC is defined on HTTP/2, and kernel TCP is cheaper per byte.
Operational costs
Section titled “Operational costs”- UDP 443 must be open end to end: firewalls, security groups, the load balancer listener. Some corporate and public networks block or throttle UDP. Clients fall back silently, so the only sign is a lower HTTP/3 share in your logs. You must keep HTTP/2 or HTTP/1.1 on TCP 443 anyway.
- CPU. QUIC is implemented in user space and encrypts every packet. It generally costs more CPU per byte than kernel TCP with TLS, which has had decades of offload work.
- Load balancers. A layer 4 balancer that hashes on addresses and ports sends a migrated connection to the wrong back end. It has to route on the connection ID. Every instance behind it must be able to recognise the connection or reset it cleanly.
- Observability. A packet capture shows almost nothing: no visible sequence numbers, ACKs or stream boundaries. TCP tools and kernel counters (retransmits, conntrack TCP state) do not apply. You rely on the QUIC library’s own logs (qlog), on TLS key logging with
SSLKEYLOGFILEfor Wireshark, and on server metrics. - Path requirements. QUIC does not allow IP fragmentation of its datagrams, and the path must carry 1,200 byte UDP payloads.
- Software support varies by proxy, CDN, cloud load balancer and client library. Many stacks terminate HTTP/3 at the edge and speak HTTP/2 or HTTP/1.1 to the origin.
Compare
Section titled “Compare”The five versions
Section titled “The five versions”| HTTP/0.9 | HTTP/1.0 | HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|---|---|
| Year | 1991 | 1996 | 1997 | 2015 | 2022 |
| Specification | none | RFC 1945 | RFC 9112 | RFC 9113 | RFC 9114 |
| Transport | TCP | TCP | TCP | TCP | QUIC over UDP |
| What it fixed | a way to fetch a document | headers, status codes, content types | connection reuse, Host, chunked bodies, caching rules |
application head-of-line blocking, header overhead | transport head-of-line blocking, setup latency, network changes |
| What still hurt | HTML only | one connection per request | one request at a time per connection | TCP head-of-line blocking | UDP reachability, CPU, tooling |
HTTP/1.1, HTTP/2 and HTTP/3 side by side
Section titled “HTTP/1.1, HTTP/2 and HTTP/3 side by side”| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Encryption | optional | optional in the RFC, TLS in every browser | always, TLS 1.3 inside QUIC |
| Wire format | text | binary frames | binary frames on QUIC streams |
| Requests per connection | one at a time | many, interleaved streams | many, independent streams |
| Connections per origin (browser) | about 6 | 1 | 1 |
| Head-of-line blocking, application | yes | no | no |
| Head-of-line blocking, transport | yes, per connection | yes, hits all streams at once | no, per stream only |
| Header compression | none | HPACK | QPACK |
| Round trips before first request (new connection) | 2 with TLS 1.3, 3 with TLS 1.2 | 2 with TLS 1.3, 3 with TLS 1.2 | 1 |
| 0-RTT resumption | only with TLS 1.3 early data, TCP handshake still needed | same as HTTP/1.1 | yes, transport and TLS together |
| Flow control | TCP only | per stream and per connection, on top of TCP | per stream and per connection, in QUIC |
| Loss recovery and congestion control | kernel TCP | kernel TCP | QUIC, in user space |
| Survives a change of IP address | no | no | yes, connection IDs |
| Cancel one request | close the connection | RST_STREAM |
reset the QUIC stream |
| Server push | no | defined, disabled in Chrome since 106 | defined, with its own frames |
| Prioritisation | none | RFC 7540 tree (deprecated), RFC 9218 | RFC 9218 Priority header |
| Negotiation | default | ALPN h2 |
Alt-Svc header or HTTPS DNS record, ALPN h3 |
| Clear text mode | yes | h2c with prior knowledge |
none |
| Middlebox visibility | full without TLS | TCP state visible, payload encrypted | almost nothing visible |
| Debug by hand | nc, telnet |
curl -v, nghttp |
curl --http3, qlog |
| Best fit | simple clients, legacy, internal tools | default for web and APIs, gRPC | public web traffic to browsers and mobile apps |
Three kinds of head-of-line blocking
Section titled “Three kinds of head-of-line blocking”| Where | Version affected | Cause | Fixed by |
|---|---|---|---|
| Application | HTTP/1.1 | responses must come back in request order on a connection | HTTP/2 streams |
| Transport | HTTP/1.1 and HTTP/2 | TCP delivers one ordered byte stream, a gap blocks everything behind it | QUIC streams |
| Header compression | HPACK if it were used over QUIC | the dynamic table needs updates in order | QPACK, which lets the encoder avoid references that could block |
What breaks
Section titled “What breaks”| Symptom | Cause | Check |
|---|---|---|
| Site never uses HTTP/3 | no alt-svc header and no HTTPS record, or UDP 443 closed |
curl -sI https://example.com/ | grep -i alt-svc, dig example.com HTTPS, curl --http3-only |
| First request is HTTP/2, later ones HTTP/3 | normal: Alt-Svc is learned from the first response | add an HTTPS DNS record to allow HTTP/3 on the first request |
--http3-only times out, --http2 works |
UDP 443 blocked by a firewall, security group or missing UDP listener | nc -vzu example.com 443 is not conclusive; capture with tcpdump -ni any udp port 443 on the server |
| HTTP/3 works on one network, not another | that network drops UDP | compare networks; the client should fall back to TCP |
Clients negotiate http/1.1 instead of h2 |
the proxy does not offer h2 in ALPN, or a TLS-inspecting middlebox strips it |
openssl s_client -connect example.com:443 -alpn h2,http/1.1 </dev/null | grep ALPN |
| gRPC fails through a proxy | a hop downgrades to HTTP/1.1, and gRPC needs HTTP/2 end to end | proxy protocol settings for the upstream; curl -v --http2 to each hop |
| Uploads or streams stall on HTTP/2 | a flow control window is exhausted and nobody sends WINDOW_UPDATE |
nghttp -nv https://example.com/, look at window sizes |
Requests fail in bursts with GOAWAY or REFUSED_STREAM |
the server is draining, or the concurrent stream limit is hit | curl -v shows the frame; check MAX_CONCURRENT_STREAMS in the server SETTINGS |
| Long requests break after a deploy of the load balancer | QUIC or HTTP/2 connections pinned to an instance that went away | connection draining settings, GOAWAY on shutdown |
| HTTP/1.1 page loads slowly with many assets | 6 connections per origin, each serial | enable HTTP/2 at the edge |
Which version did I get:
curl -sI https://example.com/ -o /dev/null -w '%{http_version}\n' # prints 1.1, 2 or 3curl -sIv https://example.com/ 2>&1 | grep -E 'ALPN|HTTP/'Force a version:
curl --http1.1 -sI https://example.com/curl --http2 -sI https://example.com/ # the default for https URLscurl --http2-prior-knowledge -sI http://localhost:8080/ # clear text HTTP/2, no upgradecurl --http3 -sI https://example.com/ # try QUIC, fall back to TCPcurl --http3-only -sI https://example.com/ # QUIC or fail--http3 needs a curl built with HTTP/3. Check it:
curl -V | grep -o 'HTTP3'Discovery:
curl -sI https://example.com/ | grep -i alt-svc# alt-svc: h3=":443"; ma=86400dig +short example.com HTTPS# 1 . alpn="h3,h2" ipv4hint=192.0.2.10curl --alt-svc altsvc.txt -sI https://example.com/ # remember Alt-Svc between runs and upgrade next timeTalk HTTP/1.1 by hand, to see that it is only text:
printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' | nc example.com 80In a browser, the Network tab of the developer tools has a Protocol column that shows http/1.1, h2 or h3 per request.
Keywords
Section titled “Keywords”HTTP/0.9, HTTP/1.0, HTTP/1.1, HTTP/2, HTTP/3, RFC 9110, RFC 9112, RFC 9113, RFC 9114, persistent connection, keep-alive, pipelining, head-of-line blocking, HOL blocking, 6 connections per origin, domain sharding, Host header, chunked transfer coding, SPDY, binary framing, frame, stream, multiplexing, SETTINGS, MAX_CONCURRENT_STREAMS, WINDOW_UPDATE, flow control, RST_STREAM, GOAWAY, HPACK, QPACK, static table, dynamic table, server push, 103 Early Hints, Priority header, ALPN, h2, h2c, h3, QUIC, UDP 443, RFC 9000, 1-RTT, 0-RTT, connection ID, connection migration, PATH_CHALLENGE, anti-amplification, Alt-Svc, HTTPS DNS record, qlog, SSLKEYLOGFILE, gRPC, curl –http2, curl –http3, curl –http3-only.
Sources
Section titled “Sources”- RFC 9110: HTTP Semantics
- RFC 9112: HTTP/1.1
- RFC 9113: HTTP/2
- RFC 9114: HTTP/3
- RFC 9000: QUIC, a UDP-based multiplexed and secure transport
- RFC 9001: Using TLS to secure QUIC
- RFC 7541: HPACK
- RFC 9204: QPACK
- RFC 7838: HTTP Alternative Services
- RFC 9460: SVCB and HTTPS DNS records
- RFC 9218: Extensible prioritization scheme for HTTP
- RFC 1945: HTTP/1.0
- MDN: Evolution of HTTP
- MDN: Connection management in HTTP/1.x
- Chrome for Developers: Removing HTTP/2 Server Push from Chrome
- curl: HTTP/3 with curl
- curl manual