HTTP Versions — 1.0, 1.1, 2, 3
Why HTTP kept getting rewritten: each version fixed a performance ceiling of the last. Knowing the progression — and which problem each version solved — is the senior-level answer.
HTTP/1.0 — one request per connection
Open a TCP connection, send one request, get one response, close the connection. Every request paid a full TCP handshake (and TLS handshake, if HTTPS). For a page with 30 assets, that’s 30 connection setups. Unusable at modern scale.
HTTP/1.1 — persistent connections, but head-of-line blocking
The version most of the web ran on for two decades. Key additions:
- Persistent connections (keep-alive) — reuse one TCP connection for many requests. No more handshake per request. This is the big one.
- Chunked transfer encoding — stream a response whose length isn’t known upfront.
- Host header — mandatory; one IP can serve many domains (virtual hosting).
- Pipelining — send multiple requests without waiting for each response. In theory. In practice it was broken by proxies and servers and is effectively dead.
The unfixed flaw: head-of-line (HOL) blocking at the HTTP layer. On a single HTTP/1.1 connection, responses must come back in order. Request A is slow → requests B and C queued behind it on that connection wait, even though they’re ready.
The browser workaround: open ~6 parallel connections per origin. Six connections = six things in flight. But it’s a hack — 6 TCP/TLS handshakes, 6× the connection state, and still only 6-wide. This connection limit is why the HTTP/1.1 era had “optimization” tricks like domain sharding, sprite sheets, and asset concatenation — all working around the 6-connection ceiling.
HTTP/2 — multiplexing over one connection
HTTP/2 (2015) keeps HTTP’s semantics (same methods, status codes, headers) but completely changes the wire format:
- Multiplexing — many concurrent streams over a single TCP connection. Request A being slow no longer blocks B and C — they’re independent streams. This kills HTTP-layer HOL blocking and makes the 6-connection hack (and domain sharding, concatenation) obsolete.
- Binary framing — the protocol is binary, not text; more efficient to parse.
- Header compression (HPACK) — HTTP headers are repetitive (same
Cookie,User-Agenton every request); HPACK compresses them, big savings on header-heavy traffic. - Server push — server could pre-emptively send resources. Mostly a failure in practice; deprecated. Don’t bring it up as a feature.
- Stream prioritization — hint which streams matter most.
The remaining flaw: HTTP/2 still runs over TCP, and TCP has its own HOL blocking. If one TCP packet is lost, every multiplexed stream on that connection stalls until the packet is retransmitted — because TCP delivers bytes strictly in order and HTTP/2’s streams share one TCP byte-stream. HTTP/2 fixed application-layer HOL blocking but inherited transport-layer HOL blocking.
HTTP/3 — over QUIC, no transport HOL blocking
HTTP/3 (2022) keeps HTTP semantics again but replaces the transport: instead of TCP, it runs over QUIC, which runs over UDP.
Why QUIC:
- No transport-layer HOL blocking — QUIC has independent streams at the transport level. A lost packet stalls only its own stream; the others keep flowing. This is the core fix.
- Faster connection setup — QUIC combines the transport and TLS handshakes into one round trip (and 0-RTT for repeat connections). TCP+TLS is 2-3 round trips.
- Connection migration — a QUIC connection has a connection ID independent of IP/port, so it survives a network change (Wi-Fi → cellular) without re-establishing. TCP connections die when the IP changes.
- TLS 1.3 is mandatory — HTTP/3 is always encrypted.
The cost: UDP is sometimes blocked or deprioritized by middleboxes/firewalls, so clients fall back to HTTP/2 over TCP when QUIC doesn’t work. And it’s newer — server/library support is still maturing.
The progression in one table
| Transport | Concurrency | HOL blocking | Handshake | |
|---|---|---|---|---|
| HTTP/1.0 | TCP | 1 request/connection | n/a (serial) | per request |
| HTTP/1.1 | TCP | 1 connection, serial responses; browsers open ~6 | HTTP-layer (ordered responses) | per connection |
| HTTP/2 | TCP | multiplexed streams, 1 connection | transport-layer (TCP packet loss stalls all streams) | TCP + TLS (2-3 RTT) |
| HTTP/3 | QUIC/UDP | multiplexed independent streams | none | combined, 1 RTT (0-RTT resumption) |
Each version’s headline is “fix the HOL blocking the previous one couldn’t”: 1.1 → reuse connections; 2 → multiplex (kills app-layer HOL); 3 → independent transport streams (kills transport-layer HOL).
What this means for a backend engineer
- Your app code is mostly version-agnostic — HTTP semantics (methods, status codes, headers) are the same across 1.1/2/3. The version is negotiated below your handler.
- Termination is usually at the edge — CloudFront / ALB / nginx speaks HTTP/2 or HTTP/3 to the client, then often plain HTTP/1.1 to your origin over the (fast, low-loss) internal network. The version that matters most is client↔edge.
- HTTP/2 to your origin matters for gRPC — gRPC requires HTTP/2 end-to-end (it relies on multiplexed streaming). If you run gRPC services, the internal hop must be HTTP/2.
- The old HTTP/1.1 optimization tricks are now anti-patterns — domain sharding, asset concatenation, and sprite sheets all worked around the 6-connection limit; under HTTP/2/3 multiplexing they hurt (sharding fragments the multiplexing benefit).
- Connection reuse still matters on the server side — your service calling other services should reuse connections (keep-alive pools); see
06_web_frameworks/aiohttp/and the HTTPS file.
Common gotchas
- Thinking HTTP/2 eliminated HOL blocking — it killed application-layer HOL but inherited TCP-layer HOL; one lost packet stalls all streams. That’s exactly what HTTP/3+QUIC fixes.
- Citing server push as an HTTP/2 win — it largely failed in practice and is deprecated.
- Keeping HTTP/1.1-era optimizations under HTTP/2 — domain sharding and concatenation fight multiplexing; remove them.
- Assuming HTTP/3 everywhere — UDP is sometimes blocked; clients fall back to HTTP/2. Support both.
- Running gRPC without HTTP/2 end-to-end — gRPC needs HTTP/2’s multiplexed streams; a 1.1-only hop breaks it.
- Confusing the version with the semantics — methods/status/headers are identical across versions; only the wire format and transport change.
Interview angle
- “What did HTTP/1.1 add over 1.0?” — persistent (keep-alive) connections — reuse one TCP connection for many requests instead of a handshake per request — plus chunked encoding and the mandatory Host header. Its unfixed flaw was application-layer head-of-line blocking, which browsers worked around by opening ~6 connections per origin.
- “What problem does HTTP/2 solve?” — application-layer head-of-line blocking. It multiplexes many independent streams over one TCP connection, so a slow request no longer blocks others — making the 6-connection hack and tricks like domain sharding obsolete. It also adds binary framing and HPACK header compression.
- “If HTTP/2 multiplexes, why was HTTP/3 needed?” — HTTP/2 still runs over TCP, which has its own head-of-line blocking: one lost packet stalls every multiplexed stream because TCP delivers bytes strictly in order. HTTP/3 runs over QUIC (on UDP), which has independent transport-level streams — a lost packet stalls only its own stream.
- “What else does QUIC give you besides no HOL blocking?” — faster handshakes (transport + TLS combined into one round trip, 0-RTT on resumption) and connection migration (a connection survives a network change like Wi-Fi→cellular because it’s identified by a connection ID, not IP/port).
- “Does the HTTP version change your application code?” — no — HTTP semantics (methods, status codes, headers) are identical across versions; only the wire format and transport change, negotiated below your handler. The exception worth knowing: gRPC requires HTTP/2 end-to-end.
- “Why are HTTP/1.1 optimization tricks now anti-patterns?” — domain sharding, asset concatenation, and sprite sheets all worked around the ~6-connection-per-origin limit. Under HTTP/2/3 multiplexing there’s no such limit, and sharding actively fragments the single-connection multiplexing benefit.