HTTP/3, powered by QUIC, is fundamentally a UDP-based transport protocol that tries to bring the best of TCP and TLS together, but without the latency baggage of TCP’s handshake.
Let’s get Caddy serving HTTP/3.
First, you need a Caddyfile. This is a basic setup that enables HTTP/3.
yourdomain.com {
respond "Hello, HTTP/3!"
}
When Caddy starts with this configuration, it will automatically attempt to serve HTTP/3 on UDP port 443. You don’t need to explicitly enable it; if your network and client support it, Caddy will use it.
To verify that HTTP/3 is working, you can use curl from a client that supports HTTP/3. Make sure your curl is compiled with HTTP/3 support (often --with-nghttp3 and --with-ngtcp2).
curl --http3 -I https://yourdomain.com
You should see output that includes HTTP/3 200. The -I flag fetches only the headers.
If you want to explicitly force Caddy to only serve HTTP/3, or to listen on a different UDP port, you can use the quic directive within your site block.
yourdomain.com {
quic {
// Optional: Specify a UDP port if not 443
// listen :4433
}
respond "Hello, HTTP/3 on a custom port!"
}
To test the custom port, you would use:
curl --http3 --port 4433 -I https://yourdomain.com
Caddy’s implementation of QUIC is built into the core server. It negotiates QUIC during the TLS handshake. If both client and server agree to use QUIC, the connection will be established over UDP. Otherwise, it will fall back to HTTP/1.1 or HTTP/2 over TCP, just like a standard HTTPS connection. This fallback is crucial for compatibility.
The magic happens in the TLS handshake. QUIC requires TLS 1.3. Caddy, by default, uses its automatic HTTPS feature, which includes obtaining and renewing Let’s Encrypt certificates. The TLS 1.3 handshake for QUIC is designed to be faster. It combines the TLS handshake with the initial connection setup, reducing round trips. Caddy handles this negotiation seamlessly.
If you are running Caddy behind a load balancer or a proxy, you need to ensure that UDP traffic on port 443 (or your custom QUIC port) is forwarded directly to Caddy. Many cloud load balancers, for instance, might only proxy TCP traffic by default. You’ll need to configure them to handle UDP. For example, in AWS, you’d set up a UDP target group and listener.
Consider a scenario where you have a firewall. You must ensure that UDP traffic on port 443 is allowed through. A common mistake is only opening port 443 for TCP, which will allow HTTPS but block HTTP/3.
# Example: Check firewall rules (Linux - iptables)
sudo iptables -L INPUT -v -n | grep 443
You’d be looking for an ACCEPT rule for UDP traffic on port 443. If it’s not there, you’d add it:
sudo iptables -A INPUT -p udp --dport 443 -j ACCEPT
The quic directive is where you’d configure specific QUIC settings if needed, though most of the time, Caddy’s defaults are robust. For instance, you can specify different TLS versions or cipher suites if you have very specific security requirements, but this is rarely necessary for standard deployments.
yourdomain.com {
quic {
tls_versions 1.2 1.3 # Example: explicitly allowing TLS 1.2 (though QUIC mandates 1.3)
# ciphersuite TLS_AES_256_GCM_SHA384 # Example: specify a cipher suite
}
reverse_proxy localhost:8080
}
A key aspect of QUIC is its stream multiplexing. Unlike HTTP/2 which multiplexes streams over a single TCP connection, QUIC multiplexes streams over a single QUIC connection. This means that if one stream experiences packet loss, it doesn’t block other streams on the same connection, a problem known as "head-of-line blocking" in TCP. Caddy’s HTTP/3 support leverages this inherent QUIC feature.
What most people miss is how Caddy manages the QUIC connection ID. Unlike TCP, which is identified by a 4-tuple (source IP, source port, destination IP, destination port), QUIC connections are identified by a Connection ID. This ID allows a client to change its IP address or port (e.g., moving from Wi-Fi to cellular) without breaking the connection. Caddy handles the lifecycle of these connection IDs automatically, ensuring seamless transitions for clients.
If you encounter issues, double-check that your curl or browser is indeed attempting HTTP/3. Browser developer tools (Network tab) will usually show the protocol version used for each request.
The next challenge you’ll likely face is optimizing QUIC performance, especially under adverse network conditions, which involves understanding Caddy’s max_concurrent_streams and initial_stream_window_size within the quic directive.