Skip to content

HTTP/2

Uvicorn has experimental support for HTTP/2, built on the zttp parser.

Experimental Feature

HTTP/2 support is currently experimental and is not enabled by default.

Overview

HTTP/2 introduces several key features:

  • Multiplexing: Multiple requests and responses can be sent simultaneously over a single TCP connection
  • Header compression: HTTP headers are compressed using HPACK, reducing overhead
  • Binary protocol: More efficient parsing compared to HTTP/1.1's text-based format

Enabling HTTP/2

HTTP/2 support requires the zttp package:

pip install zttp

To enable it, select the zttp HTTP implementation and pass --http2:

uvicorn main:app --http zttp --http2
import uvicorn

uvicorn.run("main:app", http="zttp", http2=True)

The --http2 option makes zttp serve both HTTP versions. Each connection is dispatched to HTTP/1.1 or HTTP/2 depending on what the client speaks. Without this option, zttp serves HTTP/1.1 only. This default keeps experimental HTTP/2 support opt-in.

Connection Methods

When using HTTPS, HTTP/2 is negotiated via ALPN (Application-Layer Protocol Negotiation) during the TLS handshake. This is the most common way to use HTTP/2, and the only one web browsers support.

sequenceDiagram
    participant Client
    participant Server

    Note over Client,Server: TLS Handshake with ALPN

    Client->>Server: ClientHello
    Note right of Client: ALPN: h2, http/1.1

    Server->>Client: ServerHello
    Note right of Server: ALPN: h2

    Note over Client,Server: TLS Handshake Complete

    Client->>Server: Connection Preface
    Note right of Client: PRI * HTTP/2.0
    Client->>Server: Client SETTINGS Frame
    Server->>Client: Server SETTINGS Frame

    Note over Client,Server: HTTP/2 Connection Established

    Client->>Server: HEADERS (Stream 1)
    Server->>Client: HEADERS + DATA (Stream 1)

For testing it locally, you can generate a self-signed certificate:

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"

Then create a simple ASGI application:

main.py
async def app(scope, receive, send):
    await send({"type": "http.response.start", "status": 200, "headers": []})
    await send({"type": "http.response.body", "body": b"ok"})

Run Uvicorn with --http zttp --http2 and the SSL certificate files:

uvicorn main:app --http zttp --http2 --ssl-keyfile key.pem --ssl-certfile cert.pem

You can test the connection using curl (-k skips certificate verification for self-signed certs):

curl -v --http2 -k https://localhost:8000/

h2c: HTTP/2 Cleartext with Prior Knowledge

On cleartext connections, Uvicorn accepts clients that speak HTTP/2 directly - known as "prior knowledge" h2c. The client opens the connection with the HTTP/2 preface instead of an HTTP/1.1 request, and Uvicorn switches protocols on the spot.

sequenceDiagram
    participant Client
    participant Server

    Note over Client,Server: Cleartext TCP Connection

    Client->>Server: Connection Preface
    Note right of Client: PRI * HTTP/2.0
    Client->>Server: Client SETTINGS Frame
    Server->>Client: Server SETTINGS Frame

    Note over Client,Server: HTTP/2 Connection Established

    Client->>Server: HEADERS + DATA (Stream 1)
    Server->>Client: HEADERS + DATA (Stream 1)

Using the same main.py:

uvicorn main:app --http zttp --http2
curl -v --http2-prior-knowledge http://localhost:8000/

This is the mechanism proxies use for cleartext HTTP/2 upstreams, so HTTP/2 between a proxy and Uvicorn works without TLS. How you enable it is proxy-specific: Traefik and Caddy use h2c:// upstream URLs, while Envoy configures HTTP/2 on the cluster.

h2c is cleartext

Prior-knowledge h2c provides no transport encryption or peer authentication. Limit it to trusted private networks (e.g. the proxy-to-Uvicorn hop); use HTTP/2 over TLS for any untrusted hop.

Warning

The HTTP/1.1 Upgrade: h2c mechanism is not supported: an upgrade request is served as plain HTTP/1.1, which RFC 7540 explicitly allows. Browsers do not support h2c at all - they only speak HTTP/2 over TLS.

ASGI Scope

When a request comes in over HTTP/2, the ASGI scope has http_version set to "2":

async def app(scope, receive, send):
    assert scope["type"] == "http"
    print(f"HTTP Version: {scope['http_version']}")  # "2" for HTTP/2

Early Hints

Uvicorn advertises the http.response.early_hint ASGI extension for HTTP/2 requests. You can send one or more 103 Early Hints responses before the final response:

main.py
async def app(scope, receive, send):
    if "http.response.early_hint" in scope.get("extensions", {}):
        await send(
            {
                "type": "http.response.early_hint",
                "links": [b"</style.css>; rel=preload; as=style"],
            }
        )

    await send({"type": "http.response.start", "status": 200, "headers": []})
    await send({"type": "http.response.body", "body": b"ok"})

Each value in links becomes a separate Link header in the 103 response.

Using with Reverse Proxies

In production, Uvicorn is typically deployed behind a reverse proxy like Nginx, Caddy, or HAProxy. The proxy can terminate TLS and serve HTTP/2 to clients, while talking either HTTP/1.1 or HTTP/2 to Uvicorn.

flowchart LR
    Client <-->|HTTP/2 over TLS| Proxy
    Proxy <-->|HTTP/1.1 or HTTP/2| Uvicorn

    style Client fill:#e1f5fe
    style Proxy fill:#fff3e0
    style Uvicorn fill:#e8f5e9

Proxy HTTP/2 Upstream Support

HTTP/2 upstream refers to the protocol used between the proxy and Uvicorn. While all modern proxies support HTTP/2 for client connections, support for HTTP/2 to backend servers varies.

Multiplexing is HTTP/2's ability to send multiple requests simultaneously over a single TCP connection. Some proxies support HTTP/2 upstream but open a new connection per request, which means they don't truly multiplex.

Here's the state of proxy support at the time of writing:

Proxy HTTP/2 Upstream Multiplexing Enabled by Documentation
Envoy Yes Yes HTTP/2 protocol options on the cluster Connection Pooling Docs
Caddy Yes Yes h2c:// upstream (experimental), or versions 2 in the transport for TLS reverse_proxy Docs
HAProxy Yes Yes proto h2 on the server line (cleartext), ssl alpn h2,http/1.1 (TLS) HTTP/2 Docs
Traefik Yes Yes h2c:// service URL ServersTransport Docs
Apache Yes (2.4.19+) No h2:// / h2c:// in ProxyPass (no HTTP/1.1 fallback) mod_proxy_http2 Docs
Nginx Yes (1.29.4+) No proxy_http_version 2; (requires ngx_http_v2_module) ngx_http_proxy_module Docs

Uvicorn supports both upstream flavors: cleartext prior-knowledge h2c, and HTTP/2 over TLS via ALPN when the proxy-to-Uvicorn hop uses TLS.

Performance Considerations

HTTP/2 provides the most benefit when:

  • High latency connections: Multiplexing reduces round-trip overhead
  • Many concurrent requests: Multiple streams share a single connection
  • Large headers: HPACK compression reduces header overhead

For internal, low-latency connections (like proxy to backend), HTTP/1.1 with keepalive often performs comparably to HTTP/2. It is simpler to configure and debug, which is why it remains the recommended default for most deployments.

Response Trailers

Trailers are headers you send after the response body. You can use them for values that become available while streaming, such as a checksum or final processing statistics, without buffering the entire response first.

They also let you serve ASGI applications that implement gRPC, which reports call results through grpc-status and optional grpc-message trailers.

import uvicorn


async def app(scope, receive, send):
    trailers = "http.response.trailers" in scope.get("extensions", {})
    await send({"type": "http.response.start", "status": 200, "trailers": trailers})
    await send({"type": "http.response.body", "body": b"Hello, world!"})
    if trailers:
        await send({
            "type": "http.response.trailers",
            "headers": [(b"x-result", b"ok")],
            "more_trailers": False,
        })


if __name__ == "__main__":
    uvicorn.run(app, http="zttp", http2=True, lifespan="off")

Your client must send TE: trailers to receive them. Set more_trailers=True to send more than one trailer message; Uvicorn combines their headers and completes the response on the last message.

Current Limitations

The implementation is young, and some protocol features are not complete yet:

  • HTTP/2 server push and Expect: 100-continue are not supported.
  • WebSockets over HTTP/2 (RFC 8441 extended CONNECT) are not supported. With --http zttp --http2, WebSocket connections still work - they are served over HTTP/1.1.