HTTP/2 on Apache httpd with mod_http2, and the HTTP/3 status

Updated November 2026

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: h2c header but kept cleartext HTTP/2 “with prior knowledge”; httpd supports both, mainly for traffic between servers.

Requirements

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

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:

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.

Browser h3, h2 or http/1.1 Front proxy nginx, Caddy or CDN Apache httpd 127.0.0.1:8080 UDP + TCP 443 HTTP/1.1 TLS ends at the proxy; Apache sees the client IP via X-Forwarded-For
HTTP/3 for an Apache site: the front proxy talks QUIC to browsers and plain HTTP to Apache.

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