HTTP Versions Explained: From HTTP/0.9 to HTTP/3
A clear, complete guide to every major HTTP version — what changed, why it mattered, and how that affects real applications today.
HTTP is how browsers, mobile apps, APIs, and microservices exchange data on the web. Saying “we use HTTP” is incomplete: HTTP/1.1, HTTP/2, and HTTP/3 behave differently under load, and those differences show up as latency, connection counts, and debugging surprises.
This article is a practical tour of every major HTTP version — enough depth to reason about performance and compatibility, without requiring you to read the RFCs cover to cover.
Why HTTP versions matter
Each version kept the familiar request/response model (GET /path, status codes, headers) while changing how bytes move on the wire:
| Concern | Why version choice matters |
|---|---|
| Latency on high-RTT networks | Fewer round trips and better connection reuse matter more |
| Many small assets (JS/CSS/images) | Older HTTP suffers from head-of-line blocking and connection limits |
| Mobile networks | Packet loss and connection migration matter (HTTP/3’s strength) |
| Proxies / corporate middleboxes | Older cleartext TCP assumptions can block newer protocols |
| Observability | Debugging HTTP/2 frames or QUIC is different from reading plain text HTTP/1.1 |
You still write the same application-level HTTP most of the time. The version mostly changes the transport and framing underneath.
Quick comparison
| Version | Year (approx.) | Transport | Framing | Multiplexing | Notable ideas |
|---|---|---|---|---|---|
| HTTP/0.9 | 1991 | TCP | Plain text | No | Request line only; response is raw body |
| HTTP/1.0 | 1996 | TCP | Plain text | No | Headers, status codes, one request per connection |
| HTTP/1.1 | 1997 / refined later | TCP | Plain text | Pipelining (rarely useful) | Persistent connections, Host, chunked transfer |
| HTTP/2 | 2015 | TCP (usually TLS) | Binary frames | Yes (streams) | HPACK, single connection per origin |
| HTTP/3 | 2022 | QUIC over UDP | Binary frames | Yes | TLS 1.3 built-in, better loss recovery, connection migration |
HTTP/0.9 — the simplest web
HTTP/0.9 was almost a one-liner protocol:
- Client opens TCP to the server.
- Client sends something like
GET /index.html. - Server responds with the document body and closes the connection.
- No status codes. No headers. No methods beyond GET in practice.
It proved the idea of a simple retrieval protocol. It did not survive as a production API surface, but it explains why later versions kept the method + path shape.
HTTP/1.0 — headers and status codes arrive
HTTP/1.0 introduced the building blocks developers still use daily:
- Request line:
GET /path HTTP/1.0 - Response status:
HTTP/1.0 200 OK - Headers:
Content-Type,Content-Length,Last-Modified, and more - Methods expanding beyond GET (POST, etc.)
The big limitation
By default, each request typically used its own TCP connection. TCP handshakes (and later TLS handshakes) are expensive relative to fetching a tiny CSS file. Pages with dozens of assets paid that cost repeatedly.
HTTP/1.0 could use Connection: keep-alive in practice, but persistent connections were standardized cleanly in HTTP/1.1.
HTTP/1.1 — the long-running workhorse
HTTP/1.1 became the default language of the web for a long time. Even today, lots of backend services, health checks, curl workflows, and internal APIs speak HTTP/1.1 comfortably.
What HTTP/1.1 added (and why it stuck)
1. Persistent connections (keep-alive)
Connections stay open by default so many requests can reuse one TCP (+ TLS) session.
2. Mandatory Host header
Required for virtual hosting — many sites on one IP.
3. Chunked transfer encoding
Stream a response without knowing Content-Length up front.
4. Better caching headers and content negotiationCache-Control, ETag, Accept, Accept-Encoding, and related machinery matured here.
5. Pipelining
Send multiple requests without waiting for each response. In theory helpful; in practice, head-of-line blocking and buggy proxies made pipelining unreliable. Browsers largely avoided it.
Head-of-line blocking in HTTP/1.1
On one connection, responses must come back in order. If request #1 is slow (large or generating slowly), request #2 and #3 wait — even if their responses are ready.
Browsers worked around this with multiple parallel connections per origin (historically up to ~6). That helped, but also:
- Multiplied TLS/TCP overhead
- Increased server resource usage
- Encouraged domain sharding hacks
HTTP/1.1 is still excellent for many APIs. It becomes awkward when a single page needs many concurrent downloads over high latency.
HTTP/2 — binary, multiplexed, one connection
HTTP/2 kept HTTP semantics (methods, status codes, headers) but reinvented the session layer.
Core ideas
Binary framing
Messages are broken into frames (HEADERS, DATA, SETTINGS, WINDOW_UPDATE, …). This is harder to read by eye than HTTP/1.1 text, but far more efficient for machines.
Streams and multiplexing
Many request/response exchanges share one TCP connection as independent streams. A large download should not block a small API call on the same connection the way HTTP/1.1 ordering did (at the HTTP layer).
HPACK header compression
Headers repeat a lot (cookie, user-agent, :path). HPACK compresses them with static/dynamic tables, cutting overhead for chatty APIs and asset-heavy pages.
Stream prioritization (evolved over time)
Browsers can express which resources matter first. Real-world prioritization has been revised (including later “extensible prioritization” work) because early schemes were hard to get right.
Server push (mostly cooled off)
Servers could push resources before the client asked. Cache interactions and wasted bandwidth made push less valuable than hoped; many deployments disable it.
TLS and ALPN
On the public web, HTTP/2 is typically negotiated with TLS using ALPN (h2). Cleartext h2c exists but is uncommon in browsers.
Important caveat: TCP head-of-line blocking remains
HTTP/2 fixes HTTP-level HOL blocking with streams. It does not remove TCP-level HOL blocking: a lost packet can stall the entire connection’s stream set until TCP recovers. That pain is a major motivation for HTTP/3.
Practical impact for developers
- Prefer HTTP/2 for public websites and high-fan-out clients.
- You no longer need exotic domain sharding for performance theater.
- Debugging often moves to browser DevTools Protocol panels, Wireshark, or server frame logs — not raw telnet.
- GZIP/Brotli still matter; HTTP/2 is not a replacement for body compression.
HTTP/3 — HTTP over QUIC
HTTP/3 maps HTTP semantics onto QUIC, a transport built on UDP.
Why QUIC?
TCP provides reliable ordered delivery for one bytestream. That is great for a single file, less great when many independent streams share one connection and a loss occurs.
QUIC:
- Runs mostly in user space (faster evolution than kernel TCP)
- Integrates TLS 1.3 into the handshake (fewer round trips to secure start)
- Gives each stream independent loss recovery (much better under packet loss)
- Supports connection migration (e.g., Wi‑Fi → cellular) via connection IDs instead of the TCP 4-tuple alone
- Enables 0-RTT resumption in some cases (with replay considerations)
What stays familiar
From an application developer’s seat:
- You still have requests, responses, headers, status codes
- You still terminate TLS at a load balancer / CDN / reverse proxy most of the time
- Many frameworks do not change your handler code when the edge speaks HTTP/3
What changes operationally
- UDP must be allowed on path (some corporate networks still struggle)
- Middleboxes that “understand TCP HTTP” may not understand QUIC
- CPU profiles and observability tools differ (QUIC stacks, qlog, etc.)
- Fallbacks to HTTP/2 / HTTP/1.1 remain important for reachability
When HTTP/3 shines
- Mobile users, roaming networks, lossy links
- Connection reuse across network changes
- Latency-sensitive pages where handshake costs matter
- Workloads previously hurt by TCP HOL blocking under loss
Side-by-side: transferring many resources
Imagine a page needing 30 small assets:
HTTP/1.1
Open several connections, fight the per-origin connection limit, risk queueing, pay repeated handshake costs if keep-alive is imperfect.
HTTP/2
Usually one connection, many streams, compressed headers — excellent on clean networks. A single congested TCP path can still stall everything.
HTTP/3
Many streams over QUIC; a lost packet is less likely to freeze unrelated streams. Handshake can be cheaper on repeat visits.
TLS versions and HTTP versions
They are related but not identical:
- HTTPS encrypts HTTP traffic
- Browsers effectively require TLS for HTTP/2 and HTTP/3 on the open web
- HTTP/3 assumes TLS 1.3-style cryptography as part of QUIC
- You can still serve HTTP/1.1 over TLS 1.2 in many setups (though modern configs prefer TLS 1.2+ with strong ciphers, and TLS 1.3 where possible)
Upgrade paths are often: enable modern TLS → enable HTTP/2 at the edge → enable HTTP/3 where UDP works.
What you should use today
Public websites and user-facing APIs behind a CDN
- Terminate HTTP/2 widely
- Enable HTTP/3 at the CDN/load balancer when your audience benefits and UDP paths are healthy
- Keep HTTP/1.1 fallback for older clients and odd networks
Internal microservices
- HTTP/1.1 or HTTP/2 both appear in production
- gRPC commonly relies on HTTP/2 features (streams, trailers)
- Prefer what your mesh/proxy supports cleanly; consistency beats chasing versions
Simple scripts and health checks
- HTTP/1.1 remains totally fine
- Reach for HTTP/2/3 when connection churn or concurrency becomes measurable
How version negotiation usually works
- Client connects with TLS and offers ALPN protocols (
h2,http/1.1, …). - Server selects a mutually supported protocol.
- For HTTP/3, clients often discover endpoints via Alt-Svc (or similar HTTPS DNS records in some ecosystems) after an initial connection, then switch for later requests.
You typically configure this on nginx, Envoy, Caddy, cloud load balancers, or CDN settings rather than in application business logic.
Debugging cheat sheet
| Symptom | Things to check |
|---|---|
| Site “stuck” on HTTP/1.1 | TLS config, ALPN, CDN feature flags |
| HTTP/2 enabled but no speedup | Huge single payloads, server think-time, no caching, blocking first-party API calls |
| HTTP/3 never appears | UDP blocked, Alt-Svc missing, intermediary stripping advertisements |
| Intermittent mobile failures | Flaky UDP paths, compare HTTP/2 fallback quality |
| Weird proxy errors after upgrade | Protocol-aware middleboxes; try HTTP/2 only |
Browser DevTools (Network → Protocol column), curl --http2 / --http3 (where supported), and CDN analytics are the fastest first tools.
Mental model to keep
- Semantics stayed stable: methods, status codes, headers, URLs.
- Transport & framing changed: text → binary frames → QUIC streams.
- HTTP/1.1 optimized connection reuse.
- HTTP/2 optimized many requests on one TCP connection.
- HTTP/3 optimized that story further for lossy and mobile networks by leaving TCP.
If you remember only that progression — reuse → multiplex on TCP → multiplex on QUIC — you already understand why the versions exist.
Summary
- HTTP/0.9 / 1.0: historical foundations; taught us requests, then headers/status.
- HTTP/1.1: still the reliable baseline; persistent connections and the modern header model.
- HTTP/2: binary multiplexing + HPACK; default performance protocol for many sites.
- HTTP/3: HTTP over QUIC/UDP; best under loss and roaming; growing at the edge.
For most teams: serve HTTP/2 everywhere you can, add HTTP/3 at the edge when available, keep HTTP/1.1 as fallback, and measure real user latency instead of upgrading for fashion.
