Guide
Nginx http2: Why NGINX Still Falls Back to HTTP/1.1
If NGINX keeps serving HTTP/1.1, verify the client-facing endpoint, ALPN negotiation, current directive syntax, and every proxy in front of the server.
What does the HTTP/1.1 response actually prove?
An HTTP/1.1 result proves only that the connection you tested did not negotiate HTTP/2. It does not prove that the NGINX configuration lacks HTTP/2 support, because a CDN, load balancer, TLS terminator, old client, or different virtual server may have handled the request.
Start outside the server. Run:
curl -I --http2 https://example.com/A successful result begins with HTTP/2 200, HTTP/2 301, or another HTTP/2 status. HTTP/1.1 200 OK means curl connected but the endpoint selected HTTP/1.1. curl: option --http2: the installed libcurl version does not support this means the test tool is missing HTTP/2 support, so it says nothing about the server.
Use curl -V and inspect the Features line for HTTP2. The curl command-line manual documents the --http2 option and the build-dependent protocols and features reported by curl.
Test the public hostname, not an origin IP with an improvised Host header. TLS chooses a certificate and virtual server through SNI before the HTTP header arrives, so an IP test can quietly reach the default server block.
If a CDN fronts the site, test the public edge and origin separately. The browser-to-edge protocol and edge-to-origin protocol are two different connections; HTTP/1.1 on the origin leg does not stop visitors receiving HTTP/2.
Is the current NGINX HTTP/2 configuration valid?
On NGINX 1.25.1 and later, put http2 on; in the server block and keep listen 443 ssl; separate. Older releases commonly use listen 443 ssl http2;, so copying syntax across versions can produce a warning or a failed configuration test.
For current releases, the minimal shape is:
server {
listen 443 ssl;
server_name example.com;
http2 on;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}Run nginx -v before editing, then validate and reload:
sudo nginx -t && sudo nginx -s reloadThe exact warning the "listen ... http2" directive is deprecated, use the "http2" directive instead points to old syntax on a newer binary. It is a migration warning, not evidence that negotiation failed. The ngx_http_v2_module documentation records the standalone directive as available since 1.25.1.
Do not update syntax blindly on an older distribution package. If nginx -t reports unknown directive "http2", that binary predates the standalone implementation or was built without the module. Check nginx -V for --with-http_v2_module, and use the syntax supported by that installed version.
Why does TLS and ALPN decide the protocol?
For ordinary browser traffic over HTTPS, the client and server select HTTP/2 through ALPN during the TLS handshake. If h2 is not offered or selected, the same HTTPS endpoint usually continues as HTTP/1.1.
Inspect the public endpoint directly:
openssl s_client -connect example.com:443 -servername example.com -alpn h2 </dev/null 2>/dev/null | grep -E 'ALPN protocol|Protocol|Cipher'The decisive line is ALPN protocol: h2. No ALPN negotiated usually means the tested endpoint, TLS library, reverse proxy, or listener is not advertising h2. ALPN protocol: http/1.1 means negotiation worked but selected the older protocol.
Certificate problems are separate. An expired certificate, wrong SNI name, incomplete chain, or TLS interception product can prevent a browser from reaching protocol selection cleanly. Fix the TLS error first.
Also check where TLS ends. If HAProxy, AWS Application Load Balancer, Cloudflare, a hosting panel, or another gateway owns port 443, changing the origin NGINX listener cannot alter the client-facing ALPN response. Enable HTTP/2 at that outer endpoint.
Can the wrong server block keep HTTP/2 disabled?
Yes. NGINX can accept the connection on a default virtual server that does not have HTTP/2 enabled, particularly when the hostname, IPv4 listener, IPv6 listener, or port differs from the block you edited.
Dump the loaded configuration rather than trusting a file under sites-available:
sudo nginx -TSearch the output for every listen 443, [::]:443, server_name, and http2 directive. A file can look correct while never being included, or a duplicate hostname can be selected from another file.
Then resolve the hostname from the client side:
dig +short A example.com
dig +short AAAA example.comBrowsers may prefer an IPv6 address that reaches a different machine or listener. Compare both paths with curl:
curl -I --http2 --resolve example.com:443:192.0.2.10 https://example.com/
curl -I --http2 --resolve example.com:443:[2001:db8::10] https://example.com/Replace the documentation addresses with the real A and AAAA results. Keep the hostname in the URL so SNI and certificate verification still use the correct name.
Does an HTTP/1.1 upstream mean clients lost HTTP/2?
No. NGINX can receive HTTP/2 from a browser and make a separate HTTP/1.0 or HTTP/1.1 connection to an application server; the protocols on those two legs do not have to match.
This distinction causes plenty of false alarms. A Node.js, Java, PHP-FPM gateway, or application log may report HTTP/1.1 because it sees the upstream connection from NGINX, while Chrome is using h2 on the public connection.
For an HTTP upstream, set the version required by the application rather than treating it as the browser protocol:
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://app_backend;
}HTTP/2 multiplexing still operates between the client and NGINX. NGINX translates individual requests into upstream requests and returns the responses over the multiplexed client connection.
Do not expect proxy_http_version 2 to fix this. The directive controls HTTP/1.0 or HTTP/1.1 behavior for the standard HTTP proxy module, not generic HTTP/2 transport to an arbitrary upstream. gRPC is different and uses grpc_pass with an HTTP/2-capable backend.
Can a browser show HTTP/1.1 when the server supports HTTP/2?
Yes. A browser may reuse an existing connection, pass through an enterprise proxy, negotiate with a service worker-controlled request path, or display traffic from a different hostname than the page itself.
Open Developer Tools, select the Network panel, enable the Protocol column, and reload with the panel open. Chromium usually labels HTTP/2 as h2; Firefox commonly shows HTTP/2 in request details. Check the document request and static resources separately because fonts, scripts, images, and API calls may use other domains.
Use a fresh private window only after testing with curl. Clearing the browser cache is rarely the first fix because HTTP protocol selection belongs to the connection, not the cached response body.
A corporate TLS inspection gateway can terminate the browser connection and create another connection to the site. In that case, curl on the same managed workstation may reproduce HTTP/1.1, while a test from an unaffected network negotiates h2. The gateway administrator owns that fault.
What should you check after a reload changes nothing?
Confirm that the reload reached the process serving port 443 and that the public hostname resolves to that machine. If the active binary, container, ingress controller, or load balancer is elsewhere, further edits to the local file are wasted effort.
Check the service and listening process:
sudo nginx -t
sudo systemctl status nginx
sudo ss -ltnp 'sport = :443'In containers, compare the host port mapping with the container configuration and run nginx -T inside the active container. In Kubernetes, inspect the Ingress controller configuration rather than an application pod that never terminates TLS.
Use the error log for reload failures. bind() to 0.0.0.0:443 failed (98: Address already in use) means another process owns the port. SSL_CTX_set_alpn_protos or shared-library errors point to a mismatched NGINX and TLS library installation, which is usually better fixed by installing a coherent vendor package than by chasing individual libraries.
The practical stopping rule is simple. If the installed NGINX lacks --with-http_v2_module and belongs to an obsolete operating-system release, replace or update the package rather than rebuilding a production web stack around it.
FAQ
- Does HTTP/2 require HTTPS in NGINX?
- NGINX can support cleartext HTTP/2 in specialized configurations, but mainstream browsers use HTTP/2 for websites over HTTPS with ALPN. For a public site, configure TLS on port 443 and test the negotiated protocol there.
- Why does curl report HTTP/2 while my application logs HTTP/1.1?
- Curl is reporting the client-to-NGINX connection, while the application usually logs the separate NGINX-to-upstream connection. That split is normal and does not mean browser multiplexing failed.
- Will enabling HTTP/2 make every site faster?
- No. Multiplexing and compressed headers may reduce connection overhead, but performance still depends on latency, caching, response sizes, application time, and the number of resources. Measure the actual page rather than treating the protocol label as a result.