The jump from HTTP/1.1 to HTTP/2 to HTTP/3 isn’t three unrelated redesigns — it’s the same problem, head-of-line blocking, getting attacked at a different layer each time. Understanding what each version actually fixed makes it obvious when upgrading matters and when it doesn’t.
HTTP/1.1: One Request, Then Wait
HTTP/1.1 sends requests as plain text over a TCP connection, and by default a connection handles one request at a time — the response has to come back before the next request can go out. Browsers work around this by opening multiple parallel connections per host, typically six, and pipelining (sending multiple requests without waiting) was specified but so poorly supported in practice that essentially nobody used it.
GET /style.css HTTP/1.1
Host: example.com
Connection: keep-alive
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 4021
body { margin: 0; }
This is why bundlers spent a decade concatenating JS and CSS files, sprite-sheeting icons, and inlining small assets — every extra request cost a full connection or a slot in the six-connection pool. Domain sharding (splitting assets across static1.example.com, static2.example.com) was a direct workaround for the six-connections-per-host limit.
HTTP/2: Multiplexing Over One Connection
HTTP/2 keeps the request/response model but changes the wire format entirely: everything is binary-framed, and multiple requests and responses can interleave over a single TCP connection as independent “streams.” A large response no longer blocks a small one behind it on the same connection.
| Property | HTTP/1.1 | HTTP/2 |
|---|---|---|
| Format | Text | Binary framed |
| Requests per connection | 1 at a time (browsers open ~6 connections) | Many, multiplexed |
| Header overhead | Full headers every request | HPACK compression, incremental |
| Head-of-line blocking | Per connection | Eliminated at the HTTP layer, still present at the TCP layer |
Because HTTP/2 bundling advantages made the old HTTP/1.1 optimizations counterproductive — concatenating files now hurts caching granularity for no connection benefit — a lot of “modern” frontend build advice (small, cacheable chunks over one giant bundle) only makes sense once you assume HTTP/2 or later.
The TCP Problem HTTP/2 Couldn’t Fix
Multiplexing streams over one TCP connection solves head-of-line blocking at the application layer, but TCP itself still delivers bytes in strict order. If a single packet is lost, TCP holds every stream’s data that arrived after it, even for streams unrelated to the lost packet, until retransmission fills the gap. On a lossy connection — mobile networks, congested Wi-Fi — this reintroduces exactly the blocking HTTP/2 was designed to eliminate, just one layer down.
HTTP/3: Moving Off TCP
HTTP/3 keeps HTTP/2’s semantics — multiplexed streams, header compression — but replaces TCP with QUIC, a transport built on top of UDP. QUIC implements its own reliability and ordering, but critically, it does so per-stream rather than for the whole connection. Lose a packet belonging to stream 4, and streams 1 through 3 keep flowing.
# Checking whether a server advertises HTTP/3 support
curl -sI --http2 https://example.com | grep -i alt-svc
# alt-svc: h3=":443"; ma=86400
QUIC also folds TLS 1.3 into the transport handshake itself, rather than layering it on top of an already-established TCP connection, which cuts a round trip off connection setup. And because QUIC connections are identified by a connection ID rather than the traditional IP/port tuple, a client switching networks — Wi-Fi to cellular — can keep the same connection alive instead of renegotiating from scratch, a property called connection migration.
| Property | HTTP/2 | HTTP/3 |
|---|---|---|
| Transport | TCP | QUIC over UDP |
| Head-of-line blocking | At the TCP layer, under packet loss | Eliminated per-stream |
| Handshake | TCP handshake, then TLS handshake | Combined into one |
| Network switching | Connection drops, must reconnect | Connection migration, stays alive |
When It Actually Matters
For a low-latency API on a stable data-center network, HTTP/1.1 vs HTTP/2 vs HTTP/3 is often a rounding error — the bottleneck is your database query, not the transport. The gains show up in specific conditions: many small concurrent requests (HTTP/2’s multiplexing), lossy or high-latency networks like mobile (HTTP/3’s per-stream loss recovery), and connection setup cost on repeated short-lived requests (QUIC’s combined handshake).
Takeaway
Each HTTP version chased head-of-line blocking down a layer: HTTP/1.1 blocked per connection, so browsers opened more connections; HTTP/2 multiplexed streams over one connection, but TCP still blocked the whole connection on packet loss; HTTP/3 replaced TCP with QUIC to make loss recovery per-stream instead of per-connection, and picked up a faster handshake and connection migration along the way. Enable the newer protocols by default — the cost is low — but expect the biggest wins on mobile, lossy, and request-heavy workloads rather than a fast, wired, low-request API.