HTTP/2 on Apache httpd with mod_http2, and the HTTP/3 status
Apache httpd has supported HTTP/2 since version 2.4.17 through mod_http2. Turning it on takes one line, Protocols h2 http/1.1, as long as you use HTTPS and a threaded MPM. HTTP/3 is a different story: the current 2.4.69 release (as of November 2026) has no HTTP/3 at all, so sites that want it put a QUIC-capable proxy in front of Apache.
What HTTP/2 changes
HTTP/2 (RFC 9113) keeps the same requests, responses and headers as HTTP/1.1, but sends them as binary frames. Its main gain is multiplexing: a browser can run many requests at once over one TCP connection instead of opening six connections and waiting in line on each. Two names matter in Apache configuration:
h2- HTTP/2 over TLS, chosen during the TLS handshake through ALPN. This is what browsers use; they only speak HTTP/2 over HTTPS.
h2c- HTTP/2 over plain TCP, without TLS. Browsers do not use it. RFC 9113 deprecated the old way of reaching it through an HTTP/1.1
Upgrade: h2cheader but kept cleartext HTTP/2 “with prior knowledge”; httpd supports both, mainly for traffic between servers.
Requirements
- httpd 2.4.17 or later with mod_http2, built against libnghttp2 1.2.1 or later. Every supported distribution package meets this.
- OpenSSL 1.0.2 or later (needed for ALPN), and a working HTTPS setup.
- TLS 1.2 or newer and strong ciphers.
H2ModernTLSOnly on(the default) refuses HTTP/2 on weak TLS, and browsers fall back to HTTP/1.1 quietly. This is the most common reason HTTP/2 “does not work”. - Preferably the event MPM. HTTP/2 works with all MPMs, but under prefork mod_http2 handles only one request at a time per connection, which removes the benefit.
Check which MPM you run:
apachectl -V | grep -i mpm
# Server MPM: event
If it says prefork, the usual cause is mod_php. Switch PHP to PHP-FPM first (see PHP with Apache), then change the MPM.
Turn on HTTP/2
Debian and Ubuntu
# only if you are moving off prefork (needs PHP-FPM instead of mod_php):
sudo a2dismod php8.4 mpm_prefork
sudo a2enmod mpm_event
sudo a2enmod http2
sudo apache2ctl configtest
sudo systemctl restart apache2
A change of MPM needs a full restart; a reload is not enough. Replace php8.4 with the mod_php module you have (apache2ctl -M | grep php shows it).
RHEL, AlmaLinux, Rocky Linux and Fedora
On Fedora, mod_http2 is a separate package that loads itself from /etc/httpd/conf.modules.d/10-h2.conf; it is usually installed along with httpd. The MPM is chosen in /etc/httpd/conf.modules.d/00-mpm.conf.
sudo dnf install mod_http2
sudo apachectl -M | grep http2
# http2_module (shared)
The configuration line
Put Protocols in the server configuration to cover every virtual host, or inside one <VirtualHost *:443>:
# all virtual hosts
Protocols h2 http/1.1
# or only one site
<VirtualHost *:443>
ServerName example.com
Protocols h2 http/1.1
# ... SSLEngine, certificates, DocumentRoot ...
</VirtualHost>
The order matters: the leftmost protocol the client also supports wins, so h2 goes first. If you use mod_md with the TLS-ALPN-01 check, keep acme-tls/1 in the list as well. Add h2c (Protocols h2 h2c http/1.1) only if a backend client really speaks cleartext HTTP/2. Then test and reload:
sudo apachectl configtest
sudo systemctl reload apache2 # httpd on RHEL/Fedora
Check that it works
curl -sI https://example.com/ | head -1
# HTTP/2 200
curl -s -o /dev/null -w '%{http_version}\n' https://example.com/
# 2
Your curl needs HTTP/2 support: curl -V should list HTTP2 under Features. In the access log the request line (%r in the combined format) ends in HTTP/2.0 for these requests; %H logs the protocol on its own. mod_http2 also sets the environment variable HTTP2, which you can log with %{HTTP2}e. In a browser, the Protocol column of the developer tools’ Network tab shows h2.
If you still get HTTP/1.1, work through this list: the request went to the right virtual host; Protocols is set there (a vhost value overrides the global one); mod_http2 is loaded; the MPM is not prefork; and the TLS settings are modern.
Tuning and pitfalls
- Separate worker threads. HTTP/2 requests are processed by mod_http2’s own worker threads, which do not appear in mod_status and are not counted in
ThreadsPerChild.H2MinWorkersandH2MaxWorkersset their number per child process; if you set neither, mod_http2 takesThreadsPerChildas its starting point. - Memory. HTTP/2 needs more memory per connection.
H2MaxSessionStreams(default 100 parallel requests per connection),H2WindowSize(default 65,535 bytes of request body buffered per stream) andH2StreamMaxMemSize(default 65,536 bytes of response buffered per stream) control it. The defaults suit most sites. - 421 Misdirected Request. Browsers reuse one HTTP/2 connection for every host name on the same certificate. If those virtual hosts have different TLS settings (protocols, ciphers, client certificates), httpd answers 421. Keep TLS settings identical across vhosts that share a certificate.
- Server Push is no longer used by browsers. Chrome disabled it by default in version 106, and other Chromium-based browsers such as Edge followed; RFC 9113 warns that push is “difficult to use effectively”. mod_http2 still pushes resources named in
Link: …; rel=preloadheaders unless you setH2Push Off. Use 103 Early Hints instead:H2EarlyHints ontogether withH2EarlyHint Link "</style.css>;rel=preload;as=style"(H2EarlyHintneeds 2.4.58 or later). - Misbehaving clients.
H2MaxStreamErrors(default 8) closes a connection after too many client-caused stream errors. It arrived with mod_http2 2.0.35 in httpd 2.4.66, according to CHANGES_2.4 (the directive page says “2.5.1 and later”, which refers to the development branch). - WebSockets over HTTP/2.
H2WebSockets on(2.4.58 and later, default off) accepts RFC 8441 WebSocket requests over HTTP/2 and hands them toProxyPass … upgrade=websocket. See the reverse proxy guide. - HTTP/2 to backends. mod_proxy_http2 (marked experimental) proxies with
h2://orh2c://URLs, for exampleProxyPass "/app" "h2c://app.internal:8080". The backend must speak HTTP/2; there is no fallback to HTTP/1.1.
For load tests, the h2load tool from the nghttp2 package speaks HTTP/2. The performance tuning guide covers MPM sizing.
HTTP/3 status in Apache httpd
HTTP/3 runs HTTP over QUIC, which uses UDP instead of TCP. As of November 2026:
- No release has HTTP/3. httpd 2.4.69 (October 1, 2026) is the current release. Neither its module list nor its CHANGES file mention HTTP/3 or QUIC.
- The 2.4 MPMs only handle TCP connections. An August 2026 thread on the httpd developers’ list, “Working towards HTTP3”, discussed exactly this. A pull request to let MPMs track non-TCP connections (#699) was closed without being merged; the trunk CHANGES file now lists MPM functions “to account for connections managed outside the MPM accept loop (e.g., UDP)”.
- Work has reached trunk. On October 30, 2026 a mod_http3 module built on OpenSSL’s QUIC stack and nghttp3, written during a Google Summer of Code project, was merged into the httpd development trunk (pull request #888). Its README calls it experimental, “just for tests”, and warns against production use. Trunk is the development branch (2.5.x); nothing has been announced about a backport to 2.4.x or a release date.
- Some third parties publish experimental patched builds. Treat them as test software, not something for production.
Check the httpd home page and the CHANGES file for each new release if you are waiting for HTTP/3.
HTTP/3 through a front proxy
If you need HTTP/3 now, put a server that speaks it in front of Apache and let Apache serve HTTP/1.1 or HTTP/2 behind it. A CDN in front of your site does the same job.
Apache side. Move Apache to a local port and restore the real client address with mod_remoteip (sudo a2enmod remoteip on Debian; loaded by default on Fedora):
Listen 127.0.0.1:8080
<VirtualHost 127.0.0.1:8080>
ServerName example.com
DocumentRoot /var/www/example.com
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 127.0.0.1
</VirtualHost>
Remove the old Listen 80 and Listen 443 lines (Debian: /etc/apache2/ports.conf) so that the proxy can bind those ports.
nginx. HTTP/3 in nginx comes from ngx_http_v3_module (nginx 1.25.0 and later). nginx’s documentation still calls it experimental, and it must be compiled in with --with-http_v3_module; check nginx -V.
server {
listen 443 quic reuseport;
listen 443 ssl;
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;
location / {
add_header Alt-Svc 'h3=":443"; ma=86400';
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
The Alt-Svc header tells browsers HTTP/3 is available on UDP port 443. Open that port in the firewall too (for example sudo ufw allow 443/udp).
Caddy. Caddy enables h1 h2 h3 by default and gets certificates itself, so a Caddyfile of three lines is enough:
example.com {
reverse_proxy 127.0.0.1:8080
}
Caddy sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host itself. To check HTTP/3 from the command line you need a curl built with HTTP/3 support: curl --http3 -sI https://example.com/ should print HTTP/3 200.
Sources
- HTTP/2 guide, Apache httpd 2.4 documentation
- mod_http2, Apache httpd 2.4 documentation
- mod_proxy_http2, Apache httpd 2.4 documentation
- mod_remoteip, Apache httpd 2.4 documentation
- Apache HTTP Server Project home page (2.4.69 release, October 1, 2026)
- CHANGES_2.4, Apache HTTP Server Project
- “Working towards HTTP3 – current MPM’s only support TCP connections”, dev@httpd.apache.org, August 2026
- “Merging PR888”, dev@httpd.apache.org, October 2026
- Pull request #888: mod_http3 code into trunk, apache/httpd on GitHub
- mod_http3 README_FIRST.md, apache/httpd trunk on GitHub
- CHANGES (trunk, 2.5.1), Apache HTTP Server Project
- RFC 9113: HTTP/2, RFC Editor
- Removing HTTP/2 Server Push from Chrome, Chrome for Developers
- Pull request #699: MPM support for non-TCP connections, apache/httpd on GitHub
- mod_http2 package spec, Fedora Package Sources
- Module ngx_http_v3_module, nginx documentation
- Module ngx_http_proxy_module, nginx documentation
- Global options (protocols), Caddy documentation
- reverse_proxy directive, Caddy documentation