HTTP/1.1 vs HTTP/2 vs HTTP/3: What Changed and Whether You Should Care
HTTP/2 multiplexed requests over one TCP connection; HTTP/3 moved to QUIC over UDP to escape TCP's head-of-line blocking and speed up handshakes. How each works, what they fix, what they break, how browsers discover HTTP/3, and what to actually configure for your app.
HTTP's semantics — methods, status codes, headers — haven't changed much in decades. What changed is how those messages travel. HTTP/2 and HTTP/3 are transport redesigns aimed at one enemy: latency from waiting. (What is latency?)
HTTP/1.1: one request at a time per connection
HTTP/1.1 sends requests as text over TCP. On one connection, a response must finish before the next can start (pipelining existed but was effectively unusable). Browsers worked around it by opening ~6 connections per origin and developers by bundling — sprites, concatenated JS, domain sharding.
Problems: each connection pays its own TCP + TLS handshake and slow-start; headers (cookies!) are repeated verbatim on every request; and a slow response blocks everything behind it on that connection (application-level head-of-line blocking).
HTTP/2: multiplexing over one TCP connection
HTTP/2 (2015) keeps the semantics but changes the wire format:
- Binary framing. Messages are split into frames tagged with a stream ID.
- Multiplexing. Many requests and responses interleave on one connection concurrently. No more 6-connection limit, no need to shard domains.
- HPACK header compression. Repeated headers are sent as small references to a shared table.
- Stream prioritisation (the original scheme was complex and widely ignored; replaced by the simpler Extensible Priorities — the
priorityheader — in RFC 9218). - Server push — sending resources before they're requested. In practice it rarely helped and browsers removed support;
103 Early HintswithLink: rel=preloadis the replacement.
In browsers, HTTP/2 is effectively HTTPS-only, negotiated via ALPN during the TLS handshake. (TLS handshake explained)
HTTP/2's remaining problem: TCP head-of-line blocking
All streams share one TCP connection, and TCP guarantees in-order delivery of bytes. If one packet is lost, every stream waits for its retransmission, even streams whose data arrived fine. On a clean network that's negligible; on lossy mobile or Wi-Fi networks, one HTTP/2 connection can perform worse than six HTTP/1.1 connections.
That can't be fixed inside TCP, which is implemented in operating system kernels and middleboxes everywhere. So HTTP/3 left TCP.
HTTP/3: HTTP over QUIC
QUIC (RFC 9000) is a transport protocol built on UDP, implemented in user space, with TLS 1.3 built in. HTTP/3 (RFC 9114) maps HTTP onto it.
What QUIC changes:
- Independent streams. Loss on one stream only delays that stream. No transport-level head-of-line blocking.
- Faster setup. Transport and TLS handshakes are combined: 1 RTT for a new connection (vs TCP's 1 + TLS's 1), 0-RTT for resumption (with the same replay caveats as TLS early data).
- Connection migration. Connections are identified by connection IDs, not the IP/port 4-tuple, so a phone switching from Wi-Fi to cellular can keep its connection.
- Always encrypted, including most transport headers — middleboxes can't ossify the protocol by meddling with them.
- QPACK replaces HPACK (which assumed in-order delivery).
Side by side
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Format | Text | Binary frames | Binary frames |
| Concurrency per connection | 1 request | Many streams | Many independent streams |
| Head-of-line blocking | Application + TCP | TCP | None at transport level (per-stream only) |
| Header compression | None | HPACK | QPACK |
| New connection setup (with TLS) | 2–3 RTT | 2 RTT (TLS 1.3: 2) | 1 RTT (0-RTT resume) |
| Survives network change | No | No | Yes (connection migration) |
| Encryption | Optional | Required in browsers | Always |
How browsers find HTTP/3
A browser can't know in advance that a server speaks QUIC, so the first visit usually uses HTTP/2 over TCP, and the server advertises HTTP/3:
alt-svc: h3=":443"; ma=86400
Next time, the browser tries QUIC (often racing it against TCP). Alternatively, a DNS HTTPS record can advertise alpn="h3,h2" so even the first connection can use HTTP/3.
What can go wrong
- UDP blocked or throttled. Some corporate networks block UDP/443; browsers fall back to HTTP/2 automatically, which is why you keep both.
- CPU cost. QUIC runs in user space with per-packet encryption; it uses more CPU than kernel TCP, though implementations (and kernel UDP offloads like GSO) have improved a lot.
- Observability. Encrypted transport headers mean classic packet tools see less; use qlog and server-side metrics.
- Firewall and load balancer support. Layer-4 balancers need to route by QUIC connection ID to keep migration working.
What you should actually do
For most apps: let your CDN or reverse proxy handle it. Cloudflare and other CDNs enable HTTP/3 with a toggle. Caddy serves HTTP/3 by default; Nginx supports it with listen 443 quic; plus an Alt-Svc header. Your application server behind the proxy can keep speaking HTTP/1.1 — the gains are on the long, lossy client hop. (Reverse proxies explained, what is a CDN?)
And unlearn HTTP/1.1-era hacks: domain sharding hurts on HTTP/2+ (extra connections, lost prioritisation); extreme bundling hurts caching. Fewer origins, reasonably sized chunks and good caching headers win. (HTTP caching headers, Core Web Vitals)
Verify what's negotiated in browser dev tools (Network tab → Protocol column: h2, h3), or:
curl -sI --http3 https://example.com | head -1 # needs a curl built with HTTP/3
The summary
- HTTP/2: binary multiplexing over one TCP connection, header compression — but TCP loss stalls all streams.
- HTTP/3: HTTP over QUIC/UDP — independent streams, 1-RTT setup, connection migration, always encrypted.
- Browsers discover HTTP/3 via
Alt-Svcor DNS HTTPS records and fall back to HTTP/2. - Enable it at your CDN or proxy; drop domain sharding and other HTTP/1.1 workarounds.
EasySpawn puts a modern HTTPS proxy in front of your app on your own domain, so you get current protocols and automatic certificates without touching proxy configuration. See how it works or join the waitlist.
Related: TLS 1.3 Handshake Explained · What Is Latency? · Server-Sent Events vs WebSockets · Why Is My Website Slow?
Keep reading
The Transactional Outbox Pattern: Reliable Events Without Dual Writes
Writing to your database and publishing an event can't be made atomic, so one eventually happens without the other. How the transactional outbox fixes it: polling relays vs CDC, ordering, at-least-once delivery, idempotent consumers with an inbox, cleanup, and monitoring.
Postgres as a Job Queue: FOR UPDATE SKIP LOCKED Done Properly
You may not need Redis or a broker for background jobs. How SKIP LOCKED makes Postgres a safe concurrent queue: claim/lease/ack, visibility timeouts and crash recovery, retries with backoff, LISTEN/NOTIFY, transactional enqueue, indexing, bloat, and its limits.