HTTP Protocol Versions

📖 13 min read

HTTP’s request and response semantics (methods, headers, status codes) have stayed the same across three major versions. What changed is how those requests travel over the network: how many can share a connection, what happens when a packet is lost, and how many round trips a new connection costs. Those are the properties that decide whether a newer version helps a given workload.

Three Versions, One Semantics

HTTP/1.1 Sends One Request at a Time per Connection

HTTP/1.1, first published in 1997, sends plain-text requests and responses over TCP. Connections are persistent, staying open for further requests, but each one carries one request-response exchange at a time. Pipelining, which let a client send several requests without waiting, was never reliably supported by browsers and intermediaries and is effectively unused.

So a client that needs 50 resources opens several parallel TCP connections, and browsers typically cap that at six per host. If one response is slow, every request queued behind it on that connection waits. That is HTTP-level head-of-line blocking. Workarounds grew up around it. Domain sharding spread resources across several hostnames to get more connections, and sprite sheets combined many images into one file. Both cost extra connections and cache efficiency.

HTTP/1.1 is still the universal baseline. Every client, proxy, and server speaks it, and for low-volume integrations its limits rarely matter.

HTTP/2 Multiplexes Streams over One TCP Connection

HTTP/2 (2015, revised as RFC 9113 in 2022) keeps HTTP’s semantics but changes the wire format to binary frames. Each request-response exchange gets a numbered stream, and frames from different streams interleave on a single TCP connection. A slow response on one stream no longer blocks the others at the HTTP layer, which removes the need for multiple connections and domain sharding.

Header compression with HPACK keeps a table of headers already sent on the connection, so repeated headers such as cookies, user agent, and authorization cost a few bytes after the first request instead of hundreds. APIs that make many small requests with similar headers benefit the most.

Server push, which let a server send resources before the client asked, didn’t survive practice. Servers often pushed resources the client already had cached. Chrome disabled it in 2022 (Chrome 106) and Firefox in 2024 (Firefox 132), so don’t design around it. Its replacements tell the browser what to fetch instead of sending it: preload links in the page, and the 103 Early Hints status, which lets a server name those resources before its final response is ready.

Prioritization lets a client say which streams matter most, such as render-blocking CSS before analytics scripts. HTTP/2’s original dependency-tree scheme was complex and unevenly implemented, and RFC 9113 deprecated it. The simpler Extensible Priorities scheme (RFC 9218) replaces it for both HTTP/2 and HTTP/3.

TCP Still Blocks Every Stream on a Lost Packet

HTTP/2 moves head-of-line blocking down a layer rather than removing it. TCP delivers one ordered byte stream, so when a single packet is lost, the receiving operating system holds back everything that arrived after it until the retransmission lands, even if the held data belongs to streams the lost packet never touched. On a clean network this rarely matters. On lossy networks such as congested mobile links, one connection carrying every stream can perform worse than HTTP/1.1’s six independent connections, because one loss now stalls all of them.

HTTP/3 Moves to QUIC over UDP

HTTP/3 (2022) replaces TCP with QUIC, a transport protocol that runs over UDP and provides reliability, congestion control, and encryption itself. QUIC is usually implemented in user space as a library rather than in the operating system kernel, which is part of why it evolves faster than TCP.

Streams are independent at the transport layer. QUIC tracks ordering per stream, so a lost packet delays only the streams whose data it carried. This is the main reason HTTP/3 exists, and the difference is largest where packet loss is common.

Flow

Head-of-Line Blocking by Version

What a slow response or a lost packet holds up in each HTTP version.

Head-of-line blocking in HTTP/1.1, HTTP/2, and HTTP/3 Three rows showing data arriving on one connection over time. In HTTP/1.1, a slow response A occupies the connection and requests B and C queue behind it. In HTTP/2 over TCP, packets from streams 1, 2, and 3 interleave; when a packet for stream 2 is lost, every packet that arrives after it is held until the retransmission, whichever stream it belongs to. In HTTP/3 over QUIC, the same loss holds only the later stream 2 packet, and the packets for streams 1 and 3 are delivered. arrival order on one connection HTTP/1.1 over TCP response A (slow) B C B and C queue behind A HTTP/2 streams over TCP 1 2 3 1 3 1 2 3 every stream waits for the retransmission stream 2 packet lost HTTP/3 streams over QUIC 1 2 3 1 3 1 2 3 only stream 2 waits stream 2 packet lost delivered to the application received but held back lost

Connection setup is shorter. QUIC combines its transport handshake with the TLS 1.3 handshake, so a new connection is ready after one round trip instead of the two that TCP plus TLS 1.3 need. Resumed connections to a known server can send data in the first flight (0-RTT). Data sent that way can be replayed by an attacker, so servers should only accept requests that are safe to repeat, such as GETs, as 0-RTT. TLS 1.3 offers the same resumption over TCP, but TCP’s own handshake still costs a round trip first.

Header compression changes too. HTTP/3 uses QPACK, a variant of HPACK adapted so that a lost packet on one stream doesn’t hold up header decoding on the others.

Connections survive address changes. A TCP connection is identified by its addresses and ports, so it breaks when a phone moves from Wi-Fi to cellular and gets a new IP address. QUIC identifies a connection by a connection ID instead, so the connection can migrate to the new address and continue.

On a datacenter network with sub-millisecond round trips and almost no loss, none of this changes much, and HTTP/3 performs about the same as HTTP/2. Its advantages show up on the public internet, especially for mobile clients.

Comparing the Versions

Characteristic HTTP/1.1 HTTP/2 HTTP/3
Transport TCP TCP QUIC over UDP
Format Text Binary frames Binary frames
Concurrent requests per connection One at a time Many streams Many streams
Head-of-line blocking HTTP layer and TCP layer TCP layer Only within a single stream
Header compression None HPACK QPACK
Round trips before the first request 2 with TLS 1.3 (1 when resuming with 0-RTT), 3 with TLS 1.2 2 with TLS 1.3 (1 when resuming with 0-RTT), 3 with TLS 1.2 1 (0 when resuming with 0-RTT)
Survives a client address change No No Yes
Encryption Optional Required by browsers; an unencrypted form exists for internal use Always, TLS 1.3 built in

Browser support for HTTP/2 is universal, and all major current browsers support HTTP/3. Actual use trails support. Cloudflare’s 2025 Radar Year in Review reports 21% of requests to its network over HTTP/3, 50% over HTTP/2, and 29% over HTTP/1.x.

How a Version Gets Chosen

Upgrading HTTP versions is designed to be non-breaking. A client and server always settle on a version both support, so enabling a newer version on a server never cuts off clients that only speak an older one.

HTTP/2 Is Negotiated Inside the TLS Handshake

For HTTPS, the client lists the protocols it supports, typically h2 and http/1.1, in the TLS handshake using ALPN (Application-Layer Protocol Negotiation). The server picks one and says so in its reply. If the server only supports HTTP/1.1, the connection proceeds as HTTP/1.1, and the only difference the client sees is performance. Negotiation costs no extra round trip because it rides on a handshake that happens anyway.

HTTP/2 without TLS, called h2c, has no handshake to ride on. The original HTTP/1.1 Upgrade mechanism for starting it was rarely deployed and RFC 9113 deprecated it. A client uses h2c only with prior knowledge, meaning it is configured to know the server speaks HTTP/2 and sends HTTP/2 frames from the first byte. That makes h2c an internal option, common for gRPC between services inside a trusted network and in local development.

HTTP/3 Is Discovered, Then Tried

A client can’t negotiate HTTP/3 inside a TCP connection, because HTTP/3 doesn’t run over TCP. It has to learn that the server supports HTTP/3 before it tries, in one of two ways:

  • Alt-Svc. The client connects over TCP with HTTP/2 or HTTP/1.1 as usual, and the server’s response includes an Alt-Svc header advertising HTTP/3 on a UDP port, usually 443. The client can use QUIC for later connections.
  • DNS HTTPS records. The server’s domain publishes an HTTPS record (RFC 9460) that lists h3 among its supported protocols, so a client that looks it up can use QUIC on the very first connection.

If the QUIC attempt fails, often because a firewall blocks UDP on 443, the client falls back to TCP. That makes enabling HTTP/3 additive. Clients that support it discover it, and everyone else carries on over TCP.

Flow

Discovering HTTP/3

Alt-Svc or a DNS HTTPS record tells the client to try QUIC.

Two ways a client learns a server supports HTTP/3 First way: the client connects to the server over TCP and TLS, offering h2 and http/1.1 through ALPN, and the server's response carries an Alt-Svc header advertising h3 on UDP port 443. Second way: the client looks up the domain's DNS HTTPS record, which lists h3 and h2 among its supported protocols. Either way, the client then connects over QUIC on UDP port 443, and falls back to TCP if the QUIC attempt fails. Client browser or library Server TCP 443 and UDP 443 DNS HTTPS record for the domain TCP + TLS, ALPN offers h2 and http/1.1 response header Alt-Svc: h3=":443" QUIC on UDP 443, HTTP/3 if it fails, the client stays on TCP look up the HTTPS record alpn: h3, h2 (QUIC on the first connection)

Where the Protocol Splits in a Deployment

The Load Balancer Speaks Two Protocols

A client’s HTTP version only governs the hop to whatever terminates its connection. In a typical cloud deployment that’s a load balancer or reverse proxy, which accepts HTTP/2 or HTTP/3 from clients and opens its own connections to the backends, often over a different version.

An AWS Application Load Balancer, for example, accepts HTTP/2 from clients on HTTPS listeners and by default forwards requests to targets over HTTP/1.1. A target group can instead be set to send HTTP/2 or gRPC to targets. Under the default, a service behind the balancer receives ordinary HTTP/1.1 requests while its clients get multiplexing and header compression on the external hop, and the application code doesn’t need to know which version the client used.

Not every front end accepts HTTP/3 at all. Application Load Balancer doesn’t, while Amazon CloudFront does, and AWS Network Load Balancer can pass QUIC through to targets that terminate it themselves. Offering HTTP/3 often means putting a CDN or a QUIC-capable proxy in front of the load balancer.

C4 · Deployment

Two Protocol Hops Through a Load Balancer

Clients negotiate a version with the balancer, which forwards over its own connections.

A load balancer terminating client connections and forwarding to services Clients on the internet connect to a load balancer over HTTP/2 with TLS, or HTTP/3 where the load balancer supports it. Client connections end at the load balancer, which is the trust boundary and enforces idle timeouts, stream limits, and connection-level defenses. Inside the private network, the load balancer opens its own connections to two service instances, over HTTP/1.1 by default or HTTP/2 or gRPC when configured. The services handle request-level concerns such as size limits, authentication, and per-user rate limits. Clients browsers, apps, API consumers your private network trust boundary: client connections end Load balancer terminates TLS idle timeouts, stream limits connection-level defenses Service instance size limits, auth, per-user rate limits Service instance size limits, auth, per-user rate limits HTTP/2, or HTTP/3 if the front end supports it HTTP/1.1 by default or HTTP/2, gRPC if set

Servers and Clients Have Different Defaults

Servers and managed load balancers mostly accept HTTP/2 without configuration. Kestrel, the ASP.NET Core server, has negotiated HTTP/2 over HTTPS by default since .NET Core 3.0, and HTTP/3 is opt-in. nginx needs a single directive, and Azure Front Door accepts HTTP/2 from clients by default.

Client libraries in application code differ more:

Client Default
Browsers Negotiate HTTP/2 and HTTP/3 automatically
.NET HttpClient HTTP/1.1 unless the request version and version policy are set
Go net/http HTTP/2 over HTTPS automatically
Python requests HTTP/1.1 only. httpx supports HTTP/2 when installed with its HTTP/2 extra and enabled

For server-to-server calls, check the client library’s default rather than assuming it matches the server’s.

Connection Limits and the Trust Boundary Sit Where Connections End

HTTP/2 multiplexes many requests over a few long-lived connections, so connection lifetime, idle timeouts, and the number of concurrent streams become the main levers against abuse and resource exhaustion. Whoever terminates the client connection enforces them, along with defenses such as slow-client protection. Kestrel exposes Limits.KeepAliveTimeout for idle connections and Limits.Http2.MaxStreamsPerConnection for concurrent streams (100 by default). Behind a load balancer those client-facing limits are the balancer’s configuration, not the application’s, and the service receives requests on connections the balancer opened from inside your network rather than from arbitrary internet clients.

Request-level concerns stay with the application: request body size limits, processing timeouts, authentication, authorization, and per-user rate limiting. Knowing where the boundary sits prevents both mistakes, rebuilding connection protection the balancer already provides and assuming the balancer covers request-level checks it can’t see.

Which Version to Enable

Internal Service-to-Service Traffic

Between services you control inside a datacenter or VPC, HTTP/2 is a safe default. Current frameworks, service meshes, and cloud load balancers support it, and multiplexing plus header compression pays off for high-volume traffic made of many small requests.

Standard gRPC runs on HTTP/2 and doesn’t work over HTTP/1.1 connections, so using gRPC settles the question. gRPC-Web exists for browsers and HTTP/1.1-only paths, but it goes through a translating proxy or server support rather than being plain gRPC.

HTTP/3 adds little internally. Datacenter networks have low latency and little loss, and connection migration doesn’t matter between services with stable addresses.

Public APIs

Enable HTTP/2 for APIs that external consumers call. ALPN negotiation makes it free for clients that support it and invisible to those that don’t. HTTP/3 is safe to add for the same reason, but it needs UDP on port 443 allowed through every firewall and load balancer on the path, which isn’t always the default.

Who the consumers are decides the priority. Server-side integrations and CLI tools get most of the benefit from HTTP/2. APIs called from mobile apps and browser-based single-page applications also gain from HTTP/3’s shorter setup and loss tolerance.

Web Applications Serving Browsers

Enable HTTP/2 for any site. Pages load dozens of resources, and multiplexing them over one connection is a large improvement over HTTP/1.1’s six connections. CDNs, reverse proxies such as nginx and Caddy, and cloud load balancers generally enable it by default. Enable HTTP/3 as well where the CDN or load balancer supports it, particularly for mobile audiences.

Legacy and Constrained Environments

Some environments still need HTTP/1.1: older proxies, some embedded and IoT clients, and enterprise middleboxes that inspect traffic and mishandle binary framing. Keep HTTP/1.1 enabled and let negotiation serve each client the best version it supports.

Practical defaults

Internal services: HTTP/2. Use HTTP/1.1 only where a dependency requires it.
Public APIs: HTTP/2 with HTTP/1.1 fallback through ALPN. Add HTTP/3 when UDP 443 is open end to end.
Web frontends: HTTP/2, likely already on. HTTP/3 if the CDN or load balancer supports it.
In every case, enabling a newer version doesn't require disabling an older one.

Found this guide helpful? Share it with your team:

Share on LinkedIn