HTTP Versions Explained: From HTTP/0.9 to HTTP/3
Understand why HTTP evolved from one request per connection to multiplexed QUIC streams, using one web-page example from start to finish.
Page content
Open a web page and the browser rarely downloads one thing. It may need HTML, CSS, JavaScript, fonts, and dozens of images.
All HTTP versions can request those files. What changed over time was how efficiently the requests share a network connection.
We will use one page throughout this article:
/index.html
/style.css
/app.js
/logo.png
By watching how each HTTP version downloads these four files, we can understand why the next version had to exist.
1. What HTTP actually defines
At the application level, HTTP is a request followed by a response.
Client Server
│ │
│ GET /index.html │
│───────────────────────────────────►│
│ │
│ 200 OK + HTML │
│◄───────────────────────────────────│
A request contains a method, target, headers, and sometimes a body. A response contains a status, headers, and usually a body.
Those ideas stayed mostly stable across HTTP/1.1, HTTP/2, and HTTP/3. Your application still handles GET, POST, 404, Content-Type, and cookies.
The versions differ mainly below that layer:
HTTP semantics What the message means
Framing How messages are represented on the connection
Transport How bytes travel reliably across the network
This distinction is the key to the entire article.
2. HTTP/0.9: one tiny request
Early HTTP was almost a one-line protocol.
The client sent:
GET /index.html
The server returned the document and closed the connection.
There were:
- no response status codes;
- no headers;
- no content type;
- no request body; and
- effectively no method other than
GET.
For a web made of linked text documents, that was enough.
It immediately became limiting when the web needed images, errors, caching, and different content types. The protocol needed metadata.
3. HTTP/1.0: messages become recognizable
HTTP/1.0 introduced the shape we still recognize.
Request:
GET /index.html HTTP/1.0
Host: example.com
Response:
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 1240
<html>...</html>
Now the server could say:
- whether the request succeeded;
- what kind of content it returned;
- how large the response was;
- when the content was last modified; and
- whether it could be cached.
The connection problem
HTTP/1.0 commonly used a separate TCP connection for each resource.
Our four-file page looked roughly like this:
Open TCP → GET HTML → receive HTML → close
Open TCP → GET CSS → receive CSS → close
Open TCP → GET JS → receive JS → close
Open TCP → GET PNG → receive PNG → close
Opening a TCP connection takes network round trips. HTTPS adds a TLS handshake as well. Repeating that setup for every small file wastes time.
Some HTTP/1.0 implementations supported keep-alive, but persistent connections were not yet the clean default. HTTP/1.1 fixed that.
4. HTTP/1.1: reuse the connection
HTTP/1.1 made connections persistent by default.
The browser could now do:
Open one TCP connection
↓
GET HTML → receive HTML
↓
GET CSS → receive CSS
↓
GET JS → receive JS
↓
GET PNG → receive PNG
↓
Reuse or eventually close the connection
This removed repeated connection setup and made the web much faster.
HTTP/1.1 also standardized features still used every day:
- the required
Hostheader, allowing many sites on one IP; Cache-Control,ETag, and stronger caching behavior;- chunked transfer encoding for responses whose size is not known in advance;
- content negotiation through headers such as
Accept; and - persistent connections.
HTTP/1.1 remains a reliable and widely supported protocol.
The next problem: requests wait in line
One connection could be reused, but it did not efficiently serve many requests at the same time.
Suppose the browser sends three requests:
1. app.js slow response
2. style.css ready quickly
3. logo.png ready quickly
On an HTTP/1.1 connection, responses must arrive in request order:
app.js ████████████████████
style.css ██
logo.png ███
Even if style.css is ready, it waits behind app.js. This is head-of-line blocking at the HTTP layer.
How browsers worked around it
Browsers opened several TCP connections to the same origin:
Connection 1 → HTML
Connection 2 → CSS
Connection 3 → JavaScript
Connection 4 → Images
That added parallelism, but every connection consumed server resources and performed its own congestion control and often its own TLS setup.
HTTP/1.1 pipelining tried to send several requests without waiting, but ordered responses, proxy bugs, and difficult retry behavior kept it from broad browser adoption.
The web needed several requests to share one connection without standing in one line.
5. HTTP/2: many streams on one connection
HTTP/2 kept the same HTTP meaning but changed how messages travel.
Instead of sending each request as one plain-text block, it divides messages into small binary frames. Frames belong to independent streams.
Our page can now use one TCP connection:
One TCP connection
│
├── Stream 1: index.html
├── Stream 3: style.css
├── Stream 5: app.js
└── Stream 7: logo.png
Frames from those streams can be interleaved:
HTML frame
CSS frame
JS frame
CSS frame
PNG frame
JS frame
...
This is multiplexing. A slow JavaScript response no longer forces a ready CSS response to wait behind the complete JavaScript body.
Why binary framing helps
HTTP/1.1 messages are readable text separated by special delimiters. HTTP/2 frames have explicit types, lengths, stream ids, and flags.
Machines can parse and interleave that structure reliably. Humans no longer debug it by opening a raw TCP connection and reading lines; browser tools and protocol-aware utilities decode it for us.
Header compression
Headers repeat heavily:
Cookie: ...
User-Agent: ...
Accept: ...
Sending them in full for every request wastes bandwidth. HTTP/2 uses HPACK, which keeps shared header tables and sends compact references for repeated values.
This is especially helpful when one page makes many small requests whose headers are large relative to their bodies.
What HTTP/2 fixed
HTTP/1.1
Several connections + request queues
HTTP/2
One connection + many concurrent streams
That reduces connection overhead and removes HTTP-level head-of-line blocking.
But one problem remains underneath HTTP.
6. The TCP limitation HTTP/2 cannot remove
TCP presents one reliable, ordered byte stream.
Imagine HTTP/2 sends packets containing frames from several streams:
Packet 1 Packet 2 Packet 3 Packet 4
HTML CSS JS PNG
If Packet 2 is lost, TCP waits for it to be retransmitted before delivering later bytes to the application:
Packet 1 [missing] Packet 3 Packet 4
delivered ↓ waiting waiting
The JavaScript and image data may have arrived physically, but TCP cannot expose them yet because its byte stream must remain ordered.
This is TCP-level head-of-line blocking.
On a clean network, HTTP/2 performs extremely well. On a lossy mobile connection, one missing packet can briefly stall every HTTP/2 stream sharing that connection.
HTTP/3 changes the transport to solve this.
7. HTTP/3: HTTP over QUIC
HTTP/3 runs over QUIC, a transport built on UDP.
UDP alone does not provide reliable delivery, congestion control, or encryption. QUIC implements those features itself, mostly in user space, and gives each stream independent delivery.
QUIC connection
│
├── Stream 1: HTML
├── Stream 3: CSS
├── Stream 5: JavaScript
└── Stream 7: Image
If a CSS packet is lost:
CSS stream waits for its missing data
JavaScript stream continues
Image stream continues
The loss does not block unrelated streams.
QUIC also improves connection setup
QUIC integrates TLS 1.3 into its handshake.
Conceptually:
Traditional TCP + TLS
TCP handshake → TLS handshake → HTTP
QUIC
Combined transport + security setup → HTTP/3
Returning clients may resume even faster with 0-RTT, although 0-RTT data has replay risks and should be limited to safe operations.
Connection migration
A TCP connection is tied to source and destination IP addresses and ports. Moving from Wi-Fi to cellular usually changes that identity and breaks the connection.
QUIC uses a connection id:
Wi-Fi network
│
├── same QUIC connection id
│
Cellular network
The connection can survive the network change when both endpoints support migration. This is valuable for mobile users.
Trade-offs
HTTP/3 is not “HTTP/2 but always faster.”
It introduces:
- more CPU work in user-space QUIC implementations;
- different observability and debugging tools;
- UDP paths that some networks block or throttle;
- more complex server and load-balancer support; and
- the need for HTTP/2 or HTTP/1.1 fallback.
It shines most on lossy, high-latency, and mobile networks.
8. One page across the versions
Return to our four resources.
HTTP/1.0
Resource 1 → new connection
Resource 2 → new connection
Resource 3 → new connection
Resource 4 → new connection
The main cost is repeated setup.
HTTP/1.1
Reuse connections
Use several connections for parallelism
Requests on one connection may queue
The main cost is limited concurrency and extra connections.
HTTP/2
One TCP connection
Many multiplexed streams
Compressed headers
The main remaining weakness is that TCP packet loss can stall all streams.
HTTP/3
One QUIC connection
Many independently delivered streams
Integrated TLS 1.3
Connection migration
The trade-off is newer, more complex transport infrastructure and uneven UDP support.
The progression is:
New connection per request
↓
Reuse connections
↓
Multiplex on one TCP connection
↓
Multiplex on independent QUIC streams
9. HTTP versions and TLS versions
HTTP and TLS solve different problems.
HTTP request/response semantics
TLS encryption and peer authentication
TCP reliable ordered byte stream
QUIC secure multiplexed transport over UDP
Common combinations are:
HTTP/1.1 over TCP, with or without TLS
HTTP/2 over TCP + TLS on the public web
HTTP/3 over QUIC, with TLS 1.3 built in
The HTTP/2 specification allows cleartext h2c, but browsers normally use HTTP/2 over HTTPS. HTTP/3 always uses QUIC’s integrated encryption.
10. How the client chooses a version
You usually do not write application code that picks a protocol for every request.
For HTTP/2:
- The client starts a TLS connection.
- Through ALPN, it advertises protocols such as
h2andhttp/1.1. - The server selects one both sides support.
For HTTP/3:
- A client may first connect with HTTP/2 or HTTP/1.1.
- The server advertises HTTP/3 using
Alt-Svc. - The client tries QUIC on a later or parallel connection.
- If UDP fails, it falls back to HTTP/2 or HTTP/1.1.
CDNs, load balancers, nginx, Envoy, and Caddy usually handle this negotiation at the edge. The backend application may continue speaking HTTP/1.1 or HTTP/2 internally.
11. Which version should you use?
Public websites and APIs
Use HTTP/2 as the broad modern baseline. Enable HTTP/3 at the CDN or load balancer when it is well supported, especially for mobile or geographically distributed users. Keep HTTP/1.1 fallback.
Internal services
HTTP/1.1 is often sufficient for simple request/response services. HTTP/2 is useful when one client makes many concurrent calls or when using gRPC, streaming, and trailers.
Choose the version your proxies, service mesh, language runtime, and observability stack support reliably. Protocol consistency is often more valuable than adopting the newest version everywhere.
Scripts, webhooks, and health checks
HTTP/1.1 remains completely reasonable. A newer protocol matters only when connection setup, concurrency, or packet loss is a measured problem.
A useful decision rule
Simple, low-concurrency call HTTP/1.1 is fine
Many concurrent requests HTTP/2
Lossy/mobile/global edge traffic consider HTTP/3
Measure real-user latency before and after enabling HTTP/3. Do not upgrade only because the version number is larger.
12. Common misunderstandings
“HTTP/2 makes every request faster”
Not necessarily. It helps most when many requests share a connection. A slow database query remains slow.
“HTTP/3 uses UDP, so it is unreliable”
UDP provides the packet interface. QUIC adds reliability, congestion control, ordering within streams, and encryption above it.
“HTTP/2 removes all head-of-line blocking”
It removes HTTP/1.1 response ordering between streams. TCP-level blocking after packet loss remains.
“We should domain-shard assets for HTTP/2”
Domain sharding was an HTTP/1.1 workaround for connection limits. Under HTTP/2 it can create extra DNS, TCP, and TLS setup and reduce connection reuse.
“Server push is the main reason to use HTTP/2”
Server push often wasted bandwidth or duplicated browser-cache contents. Many deployments disable it. Multiplexing and header compression are the durable benefits.
13. Debugging
Start with the browser’s Network panel and enable the Protocol column.
Useful tools include:
Browser DevTools protocol and request timing
curl --http2 force/test HTTP/2 where supported
curl --http3 test HTTP/3 when curl includes QUIC support
Wireshark packet and protocol inspection
CDN analytics real-user protocol adoption and fallback
qlog QUIC-specific connection analysis
| Symptom | Check first |
|---|---|
| Site stays on HTTP/1.1 | TLS configuration, ALPN, CDN flags |
| HTTP/2 shows little improvement | Server time, payload size, caching, number of requests |
| HTTP/3 is never selected | UDP reachability, Alt-Svc, CDN support |
| HTTP/3 fails on some networks | HTTP/2 fallback and middlebox behavior |
| Mobile requests stall | Packet loss, reconnects, and protocol-specific metrics |
Do not diagnose a protocol issue from aggregate latency alone. Separate DNS, connection, TLS, server-processing, and transfer time.
14. The mental model to remember
Each version fixed the most expensive limitation of the previous one:
HTTP/0.9 Proved simple document retrieval
HTTP/1.0 Added real messages: status, headers, content types
HTTP/1.1 Reused connections
HTTP/2 Multiplexed many streams over one TCP connection
HTTP/3 Moved streams to QUIC so packet loss affects them independently
The application semantics remained familiar while framing and transport evolved underneath.
If you remember only one line, remember:
HTTP evolved from one request per connection, to reused connections, to multiplexed TCP streams, to independent QUIC streams.
