HTTP/2 is the second major version of the HTTP protocol, standardized in 2015 (RFC 7540, superseded by RFC 9113). It runs over a TCP connection and multiplexes many request/response pairs simultaneously, eliminating HTTP/1.1’s application-layer head-of-line blocking without eliminating TCP’s ordered-delivery blocking. The result is lower latency and less connection overhead for applications that make many concurrent requests — without changing the HTTP semantics (methods, headers, status codes) that applications already use.

See HTTP for the foundational HTTP concepts that HTTP/2 builds on.

What HTTP/1.1 Got Wrong

HTTP/1.1 has two fundamental performance problems:

  1. Ordered responses on a connection: without pipelining, a client waits for one response before sending the next request. Pipelining permits several outstanding requests, but responses must stay in request order, so a slow first response still blocks later ones. Browsers instead used several TCP connections per origin, which isolates some waiting at the cost of more connection state.

  2. Verbose headers: HTTP/1.1 headers are plain text and sent in full with every request. A typical request has 500–800 bytes of headers. For small API calls, headers can be larger than the payload.

How HTTP/2 Works

Binary framing layer HTTP/2 replaces the text-based HTTP/1.1 format with a binary framing layer. Messages are split into frames (the smallest unit of communication), each tagged with a stream ID.

Multiplexing Multiple streams share a single TCP connection. Frames from different streams are interleaved, so stream 1’s response frames can arrive between stream 3’s request frames. Multiplexing removes HTTP/1.1’s response-order dependency; streams can still wait on flow-control credit, scheduling, server work, and TCP packet recovery.

flowchart TD
  A[One TCP connection] --> B[Stream 1: GET /api/users]
  A --> C[Stream 3: GET /api/orders]
  A --> D[Stream 5: POST /api/events]
  B --> E[Frames interleaved on the wire]
  C --> E
  D --> E

HPACK header compression Headers are compressed using a static table and a connection-specific dynamic table. Sensitive values such as Authorization credentials must use the never-indexed representation rather than entering the dynamic table, because dynamic compression state is shared across messages and can leak secrets through size observations. The header name can still use a static-table index; the credential value is sent as a literal and is not reduced to a one-byte dynamic-table reference.

Server Push and Prioritization Caveats

HTTP/2 still specifies server push: a server can send a promised request and response before the client asks. The mechanism was difficult to operate well because the server lacks the browser’s complete cache state, pushed bytes compete with more urgent responses, and intermediaries handle push inconsistently. Chrome removed HTTP/2 push support, and other major browsers no longer make it a dependable web optimization. This is browser-product behavior, not an HTTP/3 deprecation of the concept; HTTP/3 also defines push.

Prefer preload hints and ordinary cacheable responses for web delivery. They let the browser decide whether and when to fetch a resource.

The original HTTP/2 dependency tree allowed clients to express stream relationships and weights, but deployments implemented it inconsistently. RFC 9218 defines the simpler extensible-priority scheme using urgency and incremental delivery. A priority signal is advice, not a guarantee: the server, proxy, and congestion controller still decide scheduling. Test the full path before relying on it for user-visible ordering.

How HTTP/2 Is Negotiated (ALPN)

A client and server don’t just “speak HTTP/2” — they have to agree to. For HTTPS (the only mode browsers allow), this happens during the TLS handshake via ALPN (Application-Layer Protocol Negotiation): the ClientHello lists supported protocols (h2, http/1.1) and the server picks one in its reply — zero extra round-trips. This is why HTTP/2 is “TLS-required in practice”: ALPN rides along on the handshake that’s already happening.

Cleartext HTTP/2 (h2c) exists via an HTTP/1.1 Upgrade header, but browsers don’t support it; it’s mostly used server-to-server (e.g. behind a load balancer, or gRPC, which runs on HTTP/2 and relies on this multiplexing).

HTTP/2 vs HTTP/1.1

FeatureHTTP/1.1HTTP/2
Connections per origin6–8 (browser workaround)1
Request multiplexingNo independent streams; optional pipelining keeps responses orderedYes
Header formatPlain text, repeated in fullBinary, HPACK compressed
Server pushNoYes (limited adoption)
Head-of-line blockingApplication layerTCP layer only
TLS requirementOptionalRequired in practice (browsers enforce)

HTTP/2 in .NET

ASP.NET Core supports HTTP/2 natively via Kestrel. Enable it in appsettings.json or Program.cs:

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(443, listenOptions =>
    {
        listenOptions.UseHttps();
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
    });
});

HttpClient still defaults requests to HTTP/1.1. Ask for HTTP/2 with a request version and policy; RequestVersionOrHigher permits negotiation upward when the server supports it:

var client = new HttpClient
{
    DefaultRequestVersion = HttpVersion.Version20,
    DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
};

Pitfalls

TCP head-of-line blocking remains HTTP/2 eliminates HTTP/1.1’s ordered-response head-of-line blocking but not TCP-layer blocking. A single lost packet can delay data for all streams on the connection until TCP retransmits it. Under high packet loss, HTTP/2 can perform worse than HTTP/1.1 with multiple connections. HTTP/3 uses QUIC streams so loss on one request stream does not block unrelated request streams at the transport layer; ordered delivery still blocks later bytes within the affected stream.

Single connection amplifies TCP congestion HTTP/1.1 uses multiple connections, so congestion on one does not affect others. HTTP/2’s single connection means a congestion event affects all streams simultaneously.

Server push cache invalidation Pushed resources may already be in the client’s cache. The server has no way to know, so it wastes bandwidth pushing resources the client doesn’t need. Most production deployments disable server push.

HTTP/3 Boundary

HTTP/3 keeps HTTP semantics but replaces HTTP/2’s framing-over-TCP with HTTP framing over QUIC. QUIC uses UDP datagrams while providing its own reliable streams, congestion control, connection IDs, and TLS 1.3 handshake.

  • Loss on one QUIC stream does not block delivery on unrelated request streams; the affected stream still has ordered-delivery head-of-line blocking, and QPACK or control-stream dependencies can delay field decoding or connection progress.
  • Connection IDs allow migration between network paths, such as Wi-Fi to cellular, without identifying the connection only by IP and port.
  • TLS 1.3 is integrated into QUIC; there is no separate plaintext QUIC mode.
  • UDP-blocking networks, middleboxes, observability tooling, CPU cost, and server/CDN support can force fallback to HTTP/2.
  • HTTP/2 server push is not a reason to migrate: major browsers removed or disabled it, and HTTP/3 also has a push mechanism that applications should not assume is useful.

Questions

References