Enable HTTP/2 and HTTP/3 for Django with Nginx
Serve a Django site over HTTP/2 and HTTP/3 (QUIC) with Nginx: version-specific directives, Alt-Svc, UDP 443, keeping HTTP/1.1 to Gunicorn, verifying with curl, and what actually gets faster.
Problem statement
Pages with many static assets load slowly on high-latency connections: the browser opens several TCP connections and still queues requests behind each other. HTTP/2 multiplexes all requests over one connection, and HTTP/3 does the same over QUIC (UDP), which avoids head-of-line blocking on packet loss and recovers better on mobile networks.
Enabling them in Nginx is a small change, but the directives differ between Nginx versions, HTTP/3 needs a build with QUIC support and an open UDP port, and none of it changes the connection between Nginx and Gunicorn. This guide enables both correctly and shows how to confirm they are used.
- Check the Nginx version and build:
nginx -V. - HTTP/2 on Nginx 1.25.1 and newer:
listen 443 ssl;plushttp2 on;. On older versions:listen 443 ssl http2;. - HTTP/3 needs Nginx 1.25 or newer built with
--with-http_v3_module: addlisten 443 quic reuseport;(once per port) and anAlt-Svcheader. - Open UDP 443 in every firewall and security group.
- Leave the Gunicorn upstream on HTTP/1.1 with keepalive.
- Verify with
curl --http2and a browser's developer tools (Protocol column).
Step-by-step solution
1. Check what your Nginx supports
nginx -v
nginx -V 2>&1 | tr ' ' '\n' | grep -E "http_v2|http_v3|openssl|ssl"
--with-http_v2_moduleis present in practically every distribution build.--with-http_v3_moduleis present in the official nginx.org packages for recent versions and in some distribution builds. If it is missing, HTTP/3 directives failnginx -t; HTTP/2 still works.
2. Enable HTTP/2
Nginx 1.25.1 and newer:
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
# ssl_certificate, proxy settings...
}
Older versions (for example 1.24 on Ubuntu 24.04):
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;
}
On 1.25.1 and newer, the http2 parameter on listen still works but logs a deprecation warning. HTTP/2 over TLS is negotiated with ALPN, which every modern OpenSSL supports. Browsers only use HTTP/2 over HTTPS.
For HTTP/2 to be negotiated on a port, it is enabled for all server blocks sharing that address and port, so enabling it for one site effectively enables it for all sites on the same listen address.
3. Enable HTTP/3
server {
listen 443 ssl;
listen [::]:443 ssl;
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
location / {
proxy_pass http://django;
# proxy headers...
}
}
- QUIC requires TLS 1.3; keep
TLSv1.3enabled. reuseportmay appear only once per address and port across all server blocks; other server blocks on the same port uselisten 443 quic;without it.- Browsers first connect over HTTP/2 or HTTP/1.1, see the
Alt-Svcheader, and switch to HTTP/3 for later requests. The first visit is never HTTP/3. - Nginx's
add_headerdirectives are not inherited into alocationthat defines its ownadd_header. If a location adds headers (for example for static files), repeatAlt-Svcthere.
Full QUIC support depends on the TLS library Nginx is built with; the nginx.org documentation lists which OpenSSL, BoringSSL, LibreSSL, or QuicTLS versions provide all features, such as 0-RTT.
4. Open UDP 443
HTTP/3 runs over UDP. Open it wherever TCP 443 is open:
sudo ufw allow 443/udp
sudo ufw status | grep 443
Also update cloud security groups and any network firewalls. If UDP is blocked, browsers silently fall back to HTTP/2, so a blocked port looks like "HTTP/3 does nothing".
5. Keep the upstream simple
Nginx speaks HTTP/1.1 to Gunicorn regardless of the client protocol. Use keepalive to avoid a new upstream connection per request:
upstream django {
server unix:/run/gunicorn/gunicorn.sock;
keepalive 32;
}
location / {
proxy_pass http://django;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Django sees SERVER_PROTOCOL as HTTP/1.1 from Gunicorn. If you want to log the client's protocol, pass $server_protocol in a header or log it in Nginx's access log.
6. Reload and verify
sudo nginx -t && sudo systemctl reload nginx
curl -sI --http2 https://example.com/ | head -1
curl -sI https://example.com/ | grep -i alt-svc
Expect HTTP/2 200 and the Alt-Svc header. If your curl is built with HTTP/3 support:
curl -sI --http3-only https://example.com/ | head -1
In Chrome or Firefox developer tools, enable the Protocol column in the Network panel and reload twice; requests should show h2 and then h3.
Add the protocol to Nginx logs to see real usage:
log_format proto '$remote_addr "$request" $status $server_protocol rt=$request_time';
Explanation
HTTP/1.1 handles one request per connection at a time, so browsers open several connections and queue the rest. HTTP/2 multiplexes many requests on one TCP connection with header compression, which removes most of that queueing. HTTP/3 carries the same multiplexing over QUIC, where a lost packet only delays the stream it belongs to rather than every stream on the connection, and connection setup is faster. Nginx terminates these protocols and forwards ordinary requests to Gunicorn, so the benefit is on the network side: many small assets, high latency, or lossy mobile networks. Server-side processing time in Django is unchanged.
Edge cases / notes
Behind a CDN or load balancer
If Cloudflare, CloudFront, or a cloud load balancer terminates client connections, enable HTTP/2 and HTTP/3 there; the protocol between the edge and your Nginx does not affect browsers. Many CDNs enable both by default.
Server push is gone
HTTP/2 server push was removed from Nginx (1.25.1) and from Chrome. Use <link rel="preload"> or 103 Early Hints where supported instead.
Rollback
Remove the quic listeners and Alt-Svc header and reload. Browsers that cached Alt-Svc fall back to TCP automatically when QUIC fails.
When to automate this
Put the listener set, Alt-Svc, and UDP firewall rule in your Nginx template and provisioning, and add curl --http2 checks to post-deploy verification. Track the share of HTTP/3.0 requests in access logs to confirm clients actually upgrade.
Next steps
- Review the rest of the server block in How to Configure Nginx for Django in Production.
- Combine with compression in Enable Gzip and Brotli Compression for Django with Nginx.
- Keep certificates valid with Configure HTTPS for Django with Let's Encrypt.
- Pair protocol gains with long-lived asset caching in Long-Lived Caching for Django Static Files.
FAQ
Does Django need changes for HTTP/2 or HTTP/3?
No. Nginx terminates both and forwards HTTP/1.1 to Gunicorn. Django sees ordinary requests.
Why doesn't my browser use HTTP/3?
The first request always uses TCP. Check the Alt-Svc header, that UDP 443 is open, and that Nginx was built with the HTTP/3 module.
Which directive enables HTTP/2 in Nginx?
http2 on; on Nginx 1.25.1 and newer; the http2 parameter on listen on older versions.
Is HTTP/2 faster for Django pages?
It speeds up loading many assets over one connection, especially on high-latency networks. It does not make Django's own response time faster.