HTTP/2 & HTTP/3
How HTTP evolved from a text protocol with one request per connection to a multiplexed binary stream, and then to a transport built on UDP that survives network switches.
HTTP/2 & HTTP/3
Think of HTTP/1.1 like a single-lane checkout: one item is processed at a time, and the customer ahead of you with a full cart makes you wait even if you only have one item. HTTP/2 opened multiple lanes. HTTP/3 rebuilt the entire store on a faster road system.
Both revisions were driven by the same pressure: the web got heavier. A typical web page in 1995 was a single HTML file. A typical page in 2025 fetches dozens of assets — HTML, JavaScript bundles, CSS, fonts, API calls, images. HTTP/1.1 was designed for the former and shows its age at the latter.
HTTP/1.1: The Bottlenecks
HTTP/1.1 (RFC 2616, 1999; refined RFC 7230–7235, 2014) improved on 1.0 by introducing persistent connections — keep-alive — so a TCP connection could serve multiple requests without re-handshaking. But it kept one constraint: strict request/response ordering. A client sends request 1, waits for response 1, then sends request 2. Head-of-line blocking at the HTTP layer.
Browsers worked around this with up to six parallel TCP connections per origin. Six connections means six sets of TCP handshakes, six TLS negotiations, six congestion-control windows starting cold. It also means multiplexing is done outside the protocol, by opening more sockets.
Headers are the other problem. Every HTTP/1.1 request re-sends all headers — User-Agent, Accept-Encoding, Cookie — in plain ASCII, even if they are identical to the previous request. A typical request header block is 400–800 bytes. On a page that makes 80 requests, that is 32–64 KB of redundant header bytes on every page load.
HTTP/2: Binary Framing and Multiplexing
HTTP/2 (RFC 9113) replaced HTTP/1.1's text protocol with a binary framing layer that splits all communication into small, typed frames. This change enables three things that are impossible in a text-based protocol.
Streams and Multiplexing
A single TCP connection is divided into streams — independent, bidirectional sequences of frames. Each request/response pair occupies its own stream. Streams are identified by an integer ID (client-initiated streams use odd IDs; server-initiated use even IDs).
Connection (single TCP socket)
│
├── Stream 1 → GET /index.html ← concurrent
├── Stream 3 → GET /app.js ← concurrent
├── Stream 5 → GET /styles.css ← concurrent
└── Stream 7 → GET /logo.png ← concurrent
Frames from different streams are interleaved on the wire, then reassembled by the receiver. A slow response on stream 1 does not prevent stream 3 from making progress. The browser needs only one TCP connection per origin instead of six.
HPACK: Header Compression
HPACK (RFC 7541) compresses headers using two mechanisms:
- Static table — a fixed list of 61 common header/value pairs (e.g., entry 2 is
:method: GET). Sending index2instead ofmethod: GETcosts 1 byte instead of 11. - Dynamic table — a per-connection table that grows as new headers are sent. Once a header has been transmitted once, subsequent requests send only its index.
In practice, repeated requests to the same origin send headers in 10–30 bytes instead of 400–800 bytes. This matters most for APIs making hundreds of small calls — auth headers, cookies, and Accept types are sent once and referenced by index thereafter.
Frame Types
HTTP/2 defines 10 frame types. The ones architects encounter:
| Frame | Purpose |
|---|---|
HEADERS |
Carries the compressed header block (starts a stream) |
DATA |
Carries the request or response body |
PRIORITY |
Hints at stream processing order (deprecated in RFC 9113) |
RST_STREAM |
Cancels a stream without closing the connection |
SETTINGS |
Negotiates connection parameters (max frame size, initial window) |
PUSH_PROMISE |
Server announces a resource it will push proactively |
GOAWAY |
Gracefully closes the connection |
WINDOW_UPDATE |
Flow control: increase the receive window for a stream or connection |
Flow Control
TCP has its own flow control (the receive window), but HTTP/2 adds per-stream flow control on top. Each stream has an independent window. A slow consumer on one stream cannot stall other streams — it just causes the server to pause DATA frames on that stream until the client sends a WINDOW_UPDATE.
Server Push
HTTP/2 allows a server to proactively send resources the client will need before it asks. When responding to GET /index.html, the server can push app.js and styles.css using PUSH_PROMISE frames — the client gets the assets before parsing the HTML and discovering the <script> tags.
In practice, server push saw limited adoption and was deprecated in Chrome. Preload hints (<link rel="preload">) and early hints (103 status code) proved more predictable because the client controls what it receives.
The Problem HTTP/2 Did Not Solve
HTTP/2 eliminated head-of-line blocking at the HTTP layer. It did not eliminate it at the TCP layer.
TCP guarantees that bytes arrive in order. If a packet is lost, TCP holds back all subsequent packets until the lost packet is retransmitted and delivered. This means a single dropped packet stalls every stream on the connection — not just the stream that owns that packet.
TCP stream (HTTP/2 on a single TCP connection)
Packet 1: Stream 1 frame ✓
Packet 2: Stream 3 frame ✓
Packet 3: Stream 5 frame ✗ ← lost, retransmitted
Packet 4: Stream 1 frame ⏸ ← held back by TCP until packet 3 arrives
Packet 5: Stream 3 frame ⏸ ← held back
On high-latency or lossy networks (mobile, satellite, cross-ocean), this TCP head-of-line blocking erases much of HTTP/2's multiplexing benefit. The fix required changing the transport layer.
HTTP/3: QUIC as the Transport
HTTP/3 (RFC 9114) moves HTTP from TCP to QUIC (RFC 9000), a new transport protocol built on UDP. QUIC implements reliability, flow control, and congestion control in userspace, but does so at the stream level rather than the connection level.
Stream-Level Reliability
In QUIC, each stream handles its own retransmission independently. A lost packet only blocks the stream that owns it — other streams continue flowing.
QUIC connection (over UDP)
Stream 1: [frame 1] [frame 2] [frame 4] → delivered in order
Stream 3: [frame 1] ✗ → retransmitting frame 2
Stream 5: [frame 1] [frame 2] [frame 3] → unaffected
This is the fundamental improvement over HTTP/2 on TCP.
Built-in TLS 1.3
HTTP/2 requires TLS as a practical matter (browsers only implement h2 over TLS). TLS and TCP are separate layers — the TLS handshake occurs after the TCP handshake, adding at minimum 1 RTT.
QUIC integrates TLS 1.3 into its own handshake. A new QUIC connection establishes both transport and encryption in 1 RTT. A resumed connection (session ticket) completes in 0 RTT — the client can send application data in the first packet, before the server responds.
HTTP/1.1 (new connection):
TCP SYN → SYN-ACK → ACK (1 RTT)
TLS ClientHello → ServerHello… (1 RTT, TLS 1.3)
HTTP request → 2 RTT before data
HTTP/3 (new connection):
QUIC Initial (crypto + transport) (1 RTT)
HTTP request → 1 RTT before data
HTTP/3 (resumed connection, 0-RTT):
QUIC Initial + HTTP request (0 RTT)
→ data arrives in first round trip
Connection Migration
TCP connections are identified by a 4-tuple: (source IP, source port, destination IP, destination port). When a mobile client moves from Wi-Fi to cellular, its IP changes, the 4-tuple becomes invalid, and every TCP connection drops. The client must reconnect, re-negotiate TLS, and resume any in-flight requests.
QUIC connections are identified by a Connection ID — an opaque byte string chosen by the client. When the network path changes, the client sends a new source IP/port but the same Connection ID. The server recognises the migration and continues without interruption. HTTP/3 downloads survive a network switch; HTTP/2 downloads restart.
QPACK: Header Compression for QUIC
HPACK was designed assuming in-order delivery — the dynamic table state is updated sequentially. QUIC streams are independent and can deliver headers out of order. Applying HPACK directly would cause decompression failures when a header block referencing the dynamic table arrives before the frame that updated it.
QPACK (RFC 9204) solves this with a two-stream encoder architecture: a dedicated "encoder stream" and "decoder stream" synchronize the dynamic table state independently of the request streams. Request headers can still reference the dynamic table, but only after the required table entries have been acknowledged.
HTTP/1.1 vs HTTP/2 vs HTTP/3 at a Glance
| Property | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Transport | TCP | TCP | QUIC (UDP) |
| Protocol encoding | Text | Binary frames | Binary frames |
| Connections per origin | 6 (browsers) | 1 | 1 |
| Multiplexing | No (per connection) | Yes (streams) | Yes (streams) |
| Header compression | None | HPACK | QPACK |
| HOL blocking (HTTP) | Yes | No | No |
| HOL blocking (transport) | Yes | Yes | No |
| TLS | Optional | Required (browsers) | Integrated |
| Handshake cost (new) | 2 RTT (+ TLS) | 2 RTT (+ TLS 1.3 1 RTT) | 1 RTT |
| Handshake cost (resumed) | 2 RTT | 1 RTT (TLS session) | 0 RTT |
| Connection migration | No | No | Yes |
| Server push | No | Yes (deprecated) | No (removed) |
When to Use Which
Stick with HTTP/2 when:
- You control internal microservice communication — gRPC over HTTP/2 is the standard.
- Your load balancer or reverse proxy (NGINX, HAProxy) doesn't yet support QUIC.
- Clients are non-browser (CLIs, server-to-server) and connection migration is irrelevant.
Move to HTTP/3 when:
- You serve mobile clients on variable networks — connection migration alone justifies the upgrade.
- Your API makes many small, latency-sensitive requests where 0-RTT resumption saves measurable time.
- You operate a CDN edge — all major CDN providers (Cloudflare, Fastly, AWS CloudFront) support HTTP/3.
- Packet loss is a real concern (international traffic, satellite links, congested Wi-Fi).
HTTP/3 deployment note: QUIC runs on UDP port 443. Many corporate firewalls block or rate-limit UDP, causing QUIC connections to time out. Clients fall back to HTTP/2 via the Alt-Svc header, so the failure mode is graceful — but expect 2–5% of users on restrictive networks to never see HTTP/3.
Deployment in Practice
The Alt-Svc response header is how a server advertises HTTP/3 support to clients. An HTTP/2 response includes:
Alt-Svc: h3=":443"; ma=86400
The client stores this hint (max-age 86400 seconds) and uses QUIC on the next connection to the same origin. The first connection to a new origin always uses HTTP/1.1 or HTTP/2 — there is no way to know HTTP/3 is available before contacting the server. Tools like HTTPS DNS records (Alt-Svc in DNS) are emerging to solve this.
Negotiation uses ALPN (Application-Layer Protocol Negotiation, RFC 7301) — a TLS extension that lets client and server agree on the application protocol during the TLS handshake. The client sends ["h2", "http/1.1"] in the ClientHello; the server picks the highest it supports.
References
| RFC | Title | Year |
|---|---|---|
| RFC 7541 | HPACK: Header Compression for HTTP/2 | 2015 |
| RFC 9000 | QUIC: A UDP-Based Multiplexed and Secure Transport | 2021 |
| RFC 9001 | Using TLS to Secure QUIC | 2021 |
| RFC 9002 | QUIC Loss Detection and Congestion Control | 2021 |
| RFC 9113 | HTTP/2 | 2022 |
| RFC 9114 | HTTP/3 | 2022 |
| RFC 9204 | QPACK: Field Compression for HTTP/3 | 2022 |