I just migrated this webserver to Caddy 2 and with it, enabled HTTP/3 support. This post will give a short explanation how you can do that.
Run Caddy 2#
The HTTP/3 feature is only available in caddy 2, make sure you run at least version 2 or higher.
This is bad:
caddy --version
Caddy v1.0.3 (h1:i9gRhBgvc5ifchwWtSe7pDpsdS9+Q0Rw9oYQmYUTw1w=)
This is good:
$ caddy version
v2.11.2 h1:JX2DZQNV2nDqVjxJ4FrEMUVe5C4rcrYf+VYP2Vs5lXg=
(Note how the syntax changed from --version to version).
Enable HTTP/3#
Back in 2020 you had to opt in with an experimental_http3 global option at the top of your Caddyfile. That’s no longer the case: since Caddy 2.6, HTTP/3 is enabled by default and the experimental_http3 option has been removed (leaving it in your config will stop Caddy from starting). So a plain Caddyfile is all you need:
ma.ttias.be {
root * /var/www/html/ma.ttias.be/public
file_server
encode zstd gzip
[...]
}
There’s no directive to flip anymore. The only thing left to do is make sure UDP can reach your server.
TCP and UDP#
One major change is that HTTP/3 operates on the UDP protocol instead of TCP. Most webservers will only bind on port :80 and :443 on TCP, as they don’t need to do any UDP traffic.
By default, Caddy will also only listen on TCP:
tcp6 0 0 :::80 :::* LISTEN 27373/caddy
tcp6 0 0 :::443 :::* LISTEN 27373/caddy
With HTTP/3 active, it’ll listen on UDP as well.
tcp6 0 0 :::80 :::* LISTEN 27422/caddy
tcp6 0 0 :::443 :::* LISTEN 27422/caddy
udp6 0 0 :::443 :::* 27422/caddy
(Note: if you’re used to running netstat | grep " LISTEN ", be aware that the UDP protocol doesn’t have a LISTEN state attached to it and won’t return any results).
If you see a UDP listener, you’re ready to receive connections.
Allow UDP traffic#
Since the shift from TCP to UDP might catch some firewall vendors by surprise, make sure you’re allowing 443/udp incoming to your server.
443/tcp is probably already allowed (or you wouldn’t be serving HTTPS sites today).
Test HTTP/3 connections#
Back in 2020 this part was tricky, since none of the client tooling supported HTTP/3 out of the box. That’s no longer true: curl and every major browser speak HTTP/3 by default now.
Via curl#
Curl has a --http3 flag you can use to force HTTP/3 connections.
$ curl -I --http3 https://ma.ttias.be
HTTP/3 200
alt-svc: h3=":443"; ma=2592000
server: Caddy
...
Note the alt-svc: h3=":443" header: now that HTTP/3 is finalized as RFC 9114
, the protocol id is plain h3 with no draft suffix (in 2020 this still read h3-27 for draft 27).
You’ll need a curl that was built with HTTP/3 support. When this post was written that meant a custom build, but it now ships in stock packages on several distributions (Debian 13/trixie and newer, or via the curl snap elsewhere). Check that your build lists HTTP3 under its features:
$ curl --version
curl 8.x.x ... HTTP3
Via your browser#
In 2020 this needed a nightly build (Chrome Canary started with --enable-quic --quic-version=h3-27, or Firefox Nightly with network.http.http3.enabled flipped in about:config). None of that is necessary anymore: Chrome, Firefox, Safari and Edge all ship with HTTP/3 enabled by default.
Open your browser’s developer tools, go to the Network tab and make sure the Protocol column is shown. Reload your site and you’ll see h3 listed for the requests served over HTTP/3.
The first page load shows HTTP/2. From then on, the browser has received the alt-svc header to indicate there is HTTP/3 support and will retry the next request over UDP & HTTP/3.


Conclusion#
When I first wrote this in 2020, HTTP/3 was very experimental: server-side support was slowly coming and client-side support was disabled by default everywhere. That chicken-and-egg situation is over. HTTP/3 was finalized as RFC 9114 in 2022, Caddy turns it on by default and every major browser ships it enabled.
These days there’s nothing experimental left to do: run a recent Caddy, make sure 443/udp is open on your firewall, and you’re serving HTTP/3.