How do HTTP/1.1, HTTP/2 and HTTP/3 differ?
All three share the semantics in RFC 9110. HTTP/1.1 sends text over TCP, HTTP/2 sends binary frames over one TCP connection, HTTP/3 runs on QUIC over UDP.
Intermediate9 min read
HTTP/1.1 TCP · whole responses, in request order ● client aa─bbbbbbbbbbbbbbbbbb─cc───aa─bbbbbbbbbbbbbbbb◂ server c cannot start until all of b has arrived HTTP/2 TCP · frames of streams 1 3 5 interleaved ● client 1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─◂ server one byte stream for every request HTTP/3 QUIC over UDP · streams 0 4 8 separate 0──0──0──0──0──0──0──0──0──0──0──0──0──0──0──0 ● client 4──4──4──4──4──4──4──4──4──4──4──4──4──4──4──4◂ server 8──8──8──8──8──8──8──8──8──8──8──8──8──8──8──8 each request on its own QUIC stream ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ x lost packet dim = waiting for retransmission same HTTP semantics (RFC 9110), three wire formats
On this page
Short answer: the methods, status codes and headers are the same in all three. What changes is how a message travels. HTTP/1.1 writes it as text on a TCP connection, and one connection returns one response at a time, in request order. HTTP/2 cuts messages into binary frames and interleaves many streams on a single TCP connection. HTTP/3 sends frames over QUIC, which runs on UDP and keeps streams separate in the transport itself.
The difference shows most clearly when a packet is lost. On HTTP/2, one lost packet holds up every request on that connection. On HTTP/3, only the streams whose data was in that packet wait.
HTTP/1.1 TCP · whole responses, in request order ● client aa─bbbbbbbbbbbbbbbbbb─cc───aa─bbbbbbbbbbbbbbbb◂ server c cannot start until all of b has arrived HTTP/2 TCP · frames of streams 1 3 5 interleaved ● client 1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─◂ server one byte stream for every request HTTP/3 QUIC over UDP · streams 0 4 8 separate 0──0──0──0──0──0──0──0──0──0──0──0──0──0──0──0 ● client 4──4──4──4──4──4──4──4──4──4──4──4──4──4──4──4◂ server 8──8──8──8──8──8──8──8──8──8──8──8──8──8──8──8 each request on its own QUIC stream ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ x lost packet dim = waiting for retransmission same HTTP semantics (RFC 9110), three wire formats
What do the three versions have in common?
They share one set of semantics, defined in RFC 9110, which says it plainly: all three major versions of HTTP rely on the semantics defined by this document. In 2022 the IETF split the specification into the semantics (RFC 9110) and one document per wire format: RFC 9112 for HTTP/1.1, RFC 9113 for HTTP/2 and RFC 9114 for HTTP/3.
So GET /orders answered with 200 OK means the same thing whichever version carried it, and most application code never needs to know. RFC 9110 also notes that the versions have not obsoleted each other because each one has specific benefits and limitations depending on the context of use. That is why you still find all three in the same network.
Everything below is about the parts that do differ: message format, concurrency, header compression and the transport underneath.
How does HTTP/1.1 send requests and responses?
As readable text. RFC 9112 defines a message as a start-line, zero or more header field lines, an empty line indicating the end of the header section, and an optional message body. Capture plain http:// traffic and this is exactly what you see:
GET /orders HTTP/1.1
Host: api.example.com
Accept: application/json
HTTP/1.1 defaults to the use of persistent connections, so one TCP connection can carry many requests in a row. Within a connection, though, responses still come back one by one. A client may pipeline, sending several requests without waiting, but the server MUST send the corresponding responses in the same order that the requests were received (section 9.3.2).
That ordering rule is the source of application-level head-of-line blocking. RFC 9112 describes it as a request that takes significant server-side processing and/or transfers very large content would block subsequent requests on the same connection (section 9.4). If request b returns a large file, the response to c waits until all of b is sent, even if the server finished c long ago.
Clients work around this by opening more connections. The same section says multiple connections are typically used to avoid the "head-of-line blocking" problem, but each one costs server resources and can cause undesirable side effects in congested networks. RFC 9114 adds that TCP does not share congestion control across multiple connections.
What did HTTP/2 fix, and what did it leave?
HTTP/2 removed head-of-line blocking at the HTTP layer. It allows interleaving of messages on the same connection and uses an efficient coding for HTTP fields. It still runs on a single TCP connection, so head-of-line blocking at the TCP layer remains.
Frames and streams
Everything in HTTP/2 is a binary frame. All frames begin with a fixed 9-octet header: Length (24 bits), Type (8), Flags (8), one reserved bit and a 31-bit Stream Identifier, followed by the payload.
A request and its response share a stream. Streams opened by the client MUST use odd-numbered stream identifiers, and stream 0 carries connection-level control messages. Because frames of streams 1, 3 and 5 can be interleaved, the response to c no longer has to wait for the end of b.
Headers are compressed with HPACK, which is stateful: each endpoint has an HPACK encoder context and an HPACK decoder context, so a header repeated on the next request can be sent as a short reference. Field names MUST be converted to lowercase, and the request line becomes pseudo-headers such as :method, :scheme and :authority. Connection-specific headers such as Connection, Keep-Alive, Transfer-Encoding and Upgrade MUST NOT appear in an HTTP/2 message.
Negotiation
For https://, client and server agree during the TLS handshake using ALPN and the identifier h2, and HTTP/2 over TLS MUST use TLS version 1.2 or higher. The old Upgrade: h2c path from HTTP/1.1 was never widely deployed and is deprecated. Over cleartext TCP, a client with prior knowledge sends the connection preface straight away; it starts with the string PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n and is followed by a SETTINGS frame.
Why one lost packet stalls every stream
TCP delivers a single ordered byte stream and has no idea that several HTTP/2 streams live inside it. RFC 9114 puts it this way: because the parallel nature of HTTP/2's multiplexing is not visible to TCP's loss recovery mechanisms, a lost or reordered packet causes all active transactions to experience a stall, whether or not that transaction's data was in the lost packet. RFC 9113 itself states that TCP head-of-line blocking is not addressed by this protocol.
What changes with HTTP/3 and QUIC?
HTTP/3 moves stream separation down into the transport. QUIC packets are carried in UDP datagrams, and each request-response pair consumes a single QUIC stream. Streams are independent of each other, so one stream that is blocked or suffers packet loss does not prevent progress on other streams.
RFC 9000 is precise about the limits. After a loss, only streams with data in that packet are blocked waiting for a retransmission; if one packet carried data for several streams, all of them wait. And QUIC does not provide any means of ensuring ordering between bytes on different streams. Order is kept only inside each stream.
Dropping cross-stream ordering breaks HPACK, which relies on in-order transmission of compressed field sections. HTTP/3 uses QPACK instead: table updates travel on separate unidirectional streams, and header blocks only refer to the table without changing it.
Other differences worth knowing:
- TLS is part of QUIC. The handshake is TLS 1.3 (RFC 9001), and QUIC authenticates the entirety of each packet and encrypts as much of each packet as is practical.
- Connection IDs. A connection is identified by a connection ID rather than by addresses and ports, which allows connections to survive changes to endpoint addresses (section 9), for example a phone moving from Wi-Fi to mobile data.
- Discovery. Clients use the
h3ALPN token, and a server can advertise HTTP/3 with anAlt-Svcheader such asAlt-Svc: h3=":50781". - When UDP is blocked. If a QUIC connection cannot be set up, for example because UDP is blocked, clients SHOULD attempt to use TCP-based versions of HTTP.
Side by side:
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Spec | RFC 9112 | RFC 9113 | RFC 9114 |
| Transport | TCP | TCP | QUIC over UDP |
| Message format | Text | Binary frames | Binary frames |
| Concurrency | More connections | Streams on one connection | QUIC streams |
| One lost packet | That connection waits | Every stream waits | Only streams in that packet wait |
| Header compression | None | HPACK | QPACK |
| TLS | Optional | h2 needs TLS 1.2+ |
TLS 1.3 built into QUIC |
What does a firewall or proxy see for each version?
On plain http://, a middlebox sees everything in HTTP/1.1: method, path, headers and body. On https://, whatever the version, a device that does not decrypt sees only the TLS ClientHello and transport metadata. Here is how that plays out in the cases I deal with most in network security.
HTTP/1.1 and HTTP/2 over TLS
In TLS 1.3, all handshake messages after the ServerHello are now encrypted. The ClientHello stays readable. It usually carries SNI, by which the client reveals the name of the server it intends to connect to, and ALPN, by which the client exposes the names of application-layer protocols it supports. So the device can tell that the client offered h2 or http/1.1 and which hostname it wants, but not the path or headers.
A proxy or firewall that does decrypt and re-encrypt has to speak HTTP/2 framing as well, because the inner traffic is binary frames with HPACK-compressed headers, not HTTP/1.1 text.
HTTP/3 over QUIC
First, the traffic is UDP. HTTP/3 uses UDP port 443 by convention, but RFC 9312 points out that other applications, such as SMB over QUIC, use UDP 443 too, and DNS over QUIC is deployed as well, so simple assumptions about whether a given flow is using QUIC (or indeed which application might be using QUIC) based solely upon a UDP port number may not hold.
Second, the QUIC header gives away less than TCP does. Encryption of most transport-layer control signaling means that less information is visible to the network in comparison to TCP, and the header is integrity-protected, so nothing on the path can change it. A client Initial packet exposes the Version, Source, and Destination Connection IDs without encryption. Connection IDs can change during a connection, so observing a new connection ID does not necessarily indicate a new connection.
Third, SNI is still there, but it takes work to read. The ClientHello sits inside Initial packets protected with keys derived from the Destination Connection ID field from the client's first Initial packet and a fixed salt published in RFC 9001 (section 5.2), so anyone who sees the packet can compute them. RFC 9312 describes the procedure: calculating the Initial secret to decrypt the packet payload and parsing the QUIC CRYPTO frames containing the TLS ClientHello. A device that skips this step does not see SNI for QUIC. When RFC 9312 was published in September 2022, the IETF was working to encrypt the contents of the ClientHello; once used, that would hide SNI from on-path observers over both TCP and QUIC.
What happens if you block UDP
Block UDP 443 completely and a client that follows RFC 9114 should attempt to use TCP-based versions of HTTP instead, so traffic returns to HTTP/2 or HTTP/1.1 over TLS, which existing policy already handles. The case to avoid is throttling UDP by dropping random packets. RFC 9312 explains that blocking a random fraction of QUIC packets across 4-tuples will allow many QUIC handshakes to complete, preventing TCP fallback, but these connections will suffer from severe packet loss, and recommends blocking whole flows instead.
Which version should you turn on?
There is no single version to enable while switching off the rest. The semantics are the same, and RFC 9110 expects implementations to choose the most appropriate transport and messaging syntax for their particular context. These are the questions I would answer first:
- What problem are you solving? A page that loads many small resources over HTTP/1.1 benefits from HTTP/2's streams on one connection. Users on lossy networks are the case HTTP/3 addresses, because it removes TCP head-of-line blocking.
- What sits on the path? Every load balancer, proxy and firewall in between has to support the version. If any of them drops UDP 443, clients fall back to TCP on their own, so enabling HTTP/3 on the server does not mean users get HTTP/3.
- Does security keep the same visibility? If policy or logging depends on SNI, check that your devices read SNI from QUIC Initial packets. Otherwise HTTP/3 traffic passes without being classified.
- Do your logs record the version? When troubleshooting, knowing whether a request came over
h2orh3separates transport problems from application problems quickly.
With those four answered, picking versions becomes a configuration task rather than guesswork.
Sources
- RFC 9110: HTTP Semantics
- RFC 9112: HTTP/1.1
- RFC 9113: HTTP/2
- RFC 9114: HTTP/3
- RFC 9204: QPACK: Field Compression for HTTP/3
- RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport
- RFC 9001: Using TLS to Secure QUIC
- RFC 9312: Manageability of the QUIC Transport Protocol
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
Keep reading
Related posts
Building software
RPC vs REST: what's the difference, and which should you use?
RPC calls a named procedure on a server; REST sends requests to resources through HTTP's uniform interface. A side-by-side comparison with gRPC and JSON-RPC examples, from Fielding, RFC 9110 and the specs.
Beginner10 min read
- API
- RPC
- REST
- gRPC
- JSON-RPC
- HTTP
Building software
WebSocket vs SSE vs polling: how each works and when to use it
Polling sends a new request each round, SSE holds one response open for server events, WebSocket upgrades to two-way frames. Reconnects, proxies and firewalls.
Intermediate9 min read
- WebSocket
- Server-Sent Events
- SSE
- Long Polling
- HTTP
- Reverse Proxy
- nginx
Building software
How do offline-first apps keep working without a connection?
Offline-first apps write to a local database first and sync through a queue. One save step by step, plus retries and conflicts, from Android, Flutter and MDN.
Intermediate9 min read
- offline-first
- sync
- IndexedDB
- Service Worker
- WorkManager
- Flutter
- HTTP