A protocol that grew up in public

HTTP stands for Hypertext Transfer Protocol: the request-response protocol a client uses to ask a server for a resource, and the server uses to answer. It was invented alongside the web itself around 1991, and every version since has been a response to the same pressure - pages kept getting bigger, sites kept getting busier, and the protocol kept being asked to do things its previous version made slow. Each version is defined in an RFC, a Request for Comments, the document series in which the (Internet Engineering Task Force) publishes internet standards; this article names the current one for each version, because in 2022 the whole set was reorganized and the RFC numbers most people memorized are no longer the ones in force.

HTTP/0.9 (1991): one line, one answer

The original protocol was a single line: GET /page.html, sent over a fresh TCP (Transmission Control Protocol) connection. The server replied with the bytes of the document and closed the connection. There were no headers, no status codes, no content types, and no methods other than GET - the response could only be HTML, because there was no way to say it was anything else. Nobody called it 0.9 at the time; the number was applied retroactively once a real versioning scheme existed, to give the one-line protocol a name. It is worth knowing because its skeleton - a method, a path, a response - is still visible inside every version that followed.

HTTP/1.0 (1996): headers arrive

HTTP/1.0, documented in RFC 1945 (May 1996), turned the one-liner into a real protocol. Requests gained a version number and headers; responses gained a status line ("200 OK", "404 Not Found") and headers of their own; Content-Type let a response be an image, a stylesheet, or anything else with a (Multipurpose Internet Mail Extensions) type; and POST and HEAD joined GET. What it did not fix was the connection model: one request, one TCP connection, torn down after every answer. A page with ten images meant eleven TCP handshakes, and the cost of that ceremony is what drove the next version.

HTTP/1.1 (1997-1999): the web's long workhorse

HTTP/1.1 first appeared in RFC 2068 (January 1997) and was refined into RFC 2616 (June 1999), the document a whole generation of engineers grew up quoting. Connections became persistent by default, so many requests could reuse one TCP connection. Chunked transfer encoding let a server start sending a response before knowing its length. The mandatory Host header made name-based virtual hosting possible - many websites on one IP address, the arrangement the entire hosting industry is built on. Caching got real machinery: validators, Cache-Control, conditional requests. The version's one famous failure was pipelining, which allowed several requests in flight but required answers in order, so one slow response blocked all the ones queued behind it - a first taste of head-of-line blocking, and so troublesome in practice that browsers left pipelining off. In 2014 the specification was rewritten for precision as RFCs 7230 through 7235, without changing the protocol.

HTTP/2 (2015): binary and multiplexed

HTTP/2, published as RFC 7540 (May 2015) and grown out of Google's SPDY experiment, kept HTTP's semantics - the same methods, status codes, and headers - and replaced the wire format underneath them. Messages became binary frames carried on numbered streams, many streams multiplexed over a single TCP connection, so a slow response no longer blocks the others at the HTTP layer. Headers, highly repetitive across requests, are compressed with HPACK (RFC 7541). The specification also included server push, letting a server send resources before they were asked for; it never paid off in practice, and browsers have since walked away from it. One problem remained structurally out of reach: all those streams still share one TCP connection, and TCP delivers bytes strictly in order - so a single lost packet stalls every stream until it is retransmitted. HTTP/2 removed head-of-line blocking from HTTP and left it in the transport.

HTTP/3 (2022): move the transport

HTTP/3, published as RFC 9114 (June 2022), fixes that last problem by changing what HTTP runs on. It maps HTTP onto QUIC (RFC 9000) - a transport over UDP (the User Datagram Protocol) with streams, loss recovery, and the TLS 1.3 handshake built in, described alongside its siblings in TLS 1.2 vs TLS 1.3 vs DTLS vs QUIC. Because QUIC recovers losses per stream, one lost packet stalls only its own stream: the transport-level head-of-line blocking that HTTP/2 could not escape is gone. Header compression moves from HPACK to QPACK (RFC 9204), redesigned so compression state cannot reintroduce cross-stream blocking. Encryption is not optional, because QUIC has no unencrypted mode. And since a first contact still usually begins over TCP, servers advertise their HTTP/3 endpoint through the Alt-Svc mechanism (RFC 7838) and, increasingly, through HTTPS DNS records, letting the browser switch to QUIC on the next fetch.

What each version was actually solving

The versions read as a single argument about one constraint: a request costs a round trip, and round trips are latency you cannot optimise away.

HTTP/1.0 opened a TCP connection per request and closed it after the response. A page with forty images paid forty connection setups, each with its own handshake and its own slow start. Browsers worked around it by opening several connections at once, which shifted the cost to the server and to the network rather than removing it.

HTTP/1.1 added persistent connections and pipelining. Keep-alive fixed most of the waste. Pipelining — sending several requests without waiting for each answer — was specified and then abandoned in practice, because responses had to come back in order: one slow response blocked everything queued behind it. That is head-of-line blocking, and it is the thread running through the next two versions.

It also added the pieces that made the modern web possible: the Host header, so many sites could share one address, which is what allowed virtual hosting and, later, shared-address hosting economics; chunked transfer encoding; cache validators; and byte ranges.

HTTP/2 removed the blocking at the application layer. One connection carries many interleaved streams, framed in binary rather than text. Headers stopped being retransmitted in full on every request — HPACK compresses them against a shared table, which matters enormously when a page issues hundreds of requests with nearly identical headers. Server push was included and has since been effectively abandoned: it guessed wrong often enough to waste more bandwidth than it saved.

But the blocking moved rather than vanished. TCP guarantees ordered delivery, so one lost packet stalls every stream on that connection while it is retransmitted. On a clean network HTTP/2 is faster; on a lossy one — mobile, congested links — it can be slower than several HTTP/1.1 connections.

HTTP/3 fixes that by changing transport. QUIC runs over UDP and implements streams itself, so a lost packet stalls only its own stream. The handshake merges with the cryptographic one, and connection identifiers replace the address tuple, so a session survives moving from wireless to mobile.

Semantics, syntax, and what the 2022 renumbering did

The 2022 revision separated the specifications into semantics — methods, status codes, header fields, caching, conditional requests — from the wire syntax of each version. That is not bookkeeping: it means a status code or a method is defined once and applies identically across 1.1, 2 and 3, while only the framing differs.

The practical consequence is that upgrading versions changes performance and multiplexing, not meaning. A 304 Not Modified behaves the same way everywhere; what changes is how many round trips it took to ask.

Two things did change on the wire and catch people out. Header field names are lowercase in HTTP/2 and HTTP/3 — a mixed-case name is a protocol error, which surfaces in code that compared header names literally. And the request line was replaced by pseudo-headers (:method, :scheme, :authority, :path), so tools that parse a raw request line have nothing to parse.

The 2022 reorganization: semantics vs wire syntax

The most useful thing to know about today's HTTP specifications is how they are now arranged. In June 2022 the IETF HTTP Working Group split the protocol's meaning from its transmission. RFC 9110 defines HTTP semantics - methods, status codes, header fields, content negotiation - once, for every version; RFC 9111 defines caching the same way. The wire formats then get one document each: RFC 9112 for HTTP/1.1's text syntax, RFC 9113 for HTTP/2's framing, and RFC 9114 for HTTP/3 on QUIC. Together these obsolete every prior HTTP specification - RFC 1945, 2068, 2616, the 7230 series, and 7540 alike - so a citation of RFC 2616 today is a citation of history. The working group maintains the authoritative, current list at httpwg.org/specs, and the RFC Editor is the canonical home of the documents themselves. A GET is still a GET in every version since 0.9; what changed, five times, is how the bytes that say so travel.

Operating it, and where each version still belongs

Version negotiation is worth knowing precisely because it explains what you see on the wire. HTTP/2 over TLS is chosen with during the handshake, so there is no extra round trip. HTTP/3 cannot be negotiated that way — it is a different transport — so a server advertises it with the Alt-Svc header or an HTTPS DNS record, and the client switches on a later connection. That is why a browser's first visit is frequently HTTP/2 and the second is HTTP/3.

Where each still wins. HTTP/1.1 remains the right answer for simple internal APIs, health checks and anything where a human debugs with a text client. HTTP/2 is the default for browser traffic to origins on well-behaved networks and for many service-to-service protocols. HTTP/3 pays off most on lossy and mobile networks, and on connections that migrate.

What breaks in practice, since these are the recurring tickets: middleboxes that block UDP and force a silent fallback; load balancers that terminate HTTP/2 and re-originate 1.1 to the backend, so the multiplexing benefit stops at the edge; Connection-style hop-by-hop headers, which are prohibited in HTTP/2 and rejected rather than ignored; and inspection stacks that were written to parse request lines.

Measurement changes shape too. Counting connections stops describing load once one connection carries hundreds of concurrent requests, which is the same observation as least-connections balancing losing its meaning.

Implementations, by category

  • Servers and proxies — NGINX, Apache, Caddy, HAProxy, Envoy, Traefik. HTTP/2 is universal; HTTP/3 support arrived later and unevenly, and is worth confirming per version rather than per product.
  • Delivery networks — Cloudflare, Fastly, Akamai and peers. In practice the largest deployers of HTTP/3, and frequently where a site gets it without changing its origin at all.
  • Client libraries — curl, and the language-native stacks. Support for HTTP/3 varies enough that a client's version list is a real compatibility question rather than a footnote.
  • Browsers — the reason HTTP/2 and HTTP/3 deployed at all, and the reason server push died: they set the defaults that decide which version is actually used.