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¶
h2: HTTP/2 over TLS (Recommended)¶
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:
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:
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-continueare 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.