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 Host header, 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:

  1. The client starts a TLS connection.
  2. Through ALPN, it advertises protocols such as h2 and http/1.1.
  3. The server selects one both sides support.

For HTTP/3:

  1. A client may first connect with HTTP/2 or HTTP/1.1.
  2. The server advertises HTTP/3 using Alt-Svc.
  3. The client tries QUIC on a later or parallel connection.
  4. 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
SymptomCheck first
Site stays on HTTP/1.1TLS configuration, ALPN, CDN flags
HTTP/2 shows little improvementServer time, payload size, caching, number of requests
HTTP/3 is never selectedUDP reachability, Alt-Svc, CDN support
HTTP/3 fails on some networksHTTP/2 fallback and middlebox behavior
Mobile requests stallPacket 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.