HTTP/3 and QUIC in curl 8.x — What Actually Works Today
HTTP/3 support in curl is real, shipped, and still quieter than the blog posts suggest. Before you chase h3 latency numbers, check your binary: curl --version and look for HTTP3 in the Features: line. Plenty of stock distro builds — including the curl 8.5 build on the Ubuntu box this page was written next to — have HTTP/2 but no HTTP/3 at all, and on those --http3 dies before connecting: option --http3: the installed libcurl version doesn't support this, exit code 2. The converter on the home page will happily parse an --http3 flag from a DevTools copy; it can't tell you whether your binary can execute it.
Backends: curl 8.x cut this list in half
curl implements HTTP/3 through third-party QUIC stacks, and the project has been aggressively pruning the options:
- ngtcp2 + nghttp3 — the primary, non-experimental backend. Against OpenSSL, it needs OpenSSL 3.5+ (the first OpenSSL whose QUIC API third-party stacks can actually use) or a fork: AWS-LC, BoringSSL, LibreSSL, or quictls.
- quiche — Cloudflare's Rust stack; works, but still labeled EXPERIMENTAL in curl's own docs.
- msh3 (MS quic) and OpenSSL-QUIC (OpenSSL's own API-first QUIC layer) — both removed. The OpenSSL-QUIC backend shipped experimental in 8.8-era curl and was dropped starting with 8.19 (early 2026) because the API lagged, and its throughput and memory numbers trailed ngtcp2 by factors of 3 and ~20 in the project's own benchmarks.
So if you're rebuilding curl for h3 in 2026: ngtcp2+nghttp3 against OpenSSL 3.5+ (pairing OpenSSL 3.5+ with ngtcp2 ≥ 1.12), or quiche. Anything telling you to build --with-ngtcp2 against OpenSSL 3.0 is following a stale guide — OpenSSL only got a usable third-party-facing QUIC API in 3.5.
HTTP/3 in curl remains flagged experimental overall: expect behavior changes between minor releases, and don't build a health check that alerts on curl: unknown --http3.
--http3 vs --http3-only, and the "silent" downgrade
--http3(since 7.66.0): try QUIC first, fall back to HTTP/2 or HTTP/1 on the same request. It's happy-eyeballing for transports: curl races the QUIC handshake against a delayed TCP+TLS attempt (~100 ms soft / 200 ms hard by default). Failure only happens when both die.--http3-only(since 7.88.0): QUIC or error. The diagnostic tool, not the daily driver.
The fallback is what makes --http3 "silently" use HTTP/2: nothing failed, UDP 443 was just dropped or the server never answered the Initial packet, so you got an h2 transfer and never saw a word about it. Three checks when you suspect a silent downgrade:
curl --http3 -w '%{http_version}\n' -o /dev/null -s URL— prints3or2, no vibes involved (the variable exists since 7.50).-v: an h3 connection logsConnected to ... (quic), no TCP socket line; you'll see two connect attempts when fallback engaged.- Corporate network, VPN, or a proxy in the path? curl cannot do HTTP/3 through any proxy — with
-xset, h3 is off the table entirely, and HTTPS-only plus no-proxy is a hard design constraint, not a bug.
Note --http3 is mutexed with --http2/--http1.1 in the parser — you can't demand "h3 preferred but negotiate h2 on the same socket"; the h2 attempt is a separate fallback connection, which is why you see duplicated Trying lines in verbose output.
Alt-Svc: the bootstrap path people skip
The standards-compliant route to h3 isn't a flag; it's the server telling you about it. --alt-svc cache.txt enables the Alt-Svc parser and persists the "this origin also speaks h3 on :443" advertisement across invocations (added 7.64.1). Pass --alt-svc "" for an in-memory cache only. Two consequences people misread:
- The first request still goes over h2/h1 — the upgrade applies to subsequent requests once the cache is populated. Benchmarking "HTTP/3" with a cold cache measures HTTP/2.
- The cache is per-file, not per-user-agent: point every script at one shared file or your h3 hit-rate stays near zero.
Debugging QUIC when -v stops helping
-v shows handshake order and ALPN for TCP, but for h3 it mostly proves that QUIC was used, not why it failed. Layer up:
--trace-ascii out.logfor the full request/response stream; since 8.3--trace-configlets you narrow components (ids,http/2style) instead of drowning in the whole trace.--http3-onlyconverts silent fallback into a hard error with an exit code — the fastest way to learn whether QUIC is broken or merely slow.- Frame-level h3 tracing is still immature in curl: there's no verbose equivalent of
--trace-configfor QUIC frames. When-visn't enough, capture UDP 443 (tshark -f 'udp port 443') and look for whether your Initial packet gets any reply at all — no reply is a middlebox dropping QUIC, an Initial and then silence is a server-side h3 problem. Same shape of diagnosis as when curl works but your code fails: isolate the transport before blaming the app.
What's still maturing (be honest in your rollout)
Working today on a proper ngtcp2 build: transfers, redirects, parallel -Z multiplexing over one connection, the eyeballing fallback. Still rough: proxy use (none), HTTP/3 over anything but HTTPS (impossible by design), frame-level observability, and the quiche backend's experimental label — the curl team's stated bar for dropping it is the underlying QUIC library calling itself non-beta first.
FAQ
curl --http3 "works" — how do I know it's not just h2? curl --http3 -s -o /dev/null -w '%{http_version}\n' URL. Anything other than 3 is a fallback. Then re-run with --http3-only to see the real failure.
My Docker image has curl 8.x but --http3 fails. Two different failures. option --http3: the installed libcurl version doesn't support this (exit 2) means the flag compiled but the libcurl lacks an HTTP/3 backend. Unknown option means the tool predates the flag entirely (pre-7.66). Diagnose with curl --version | grep -o HTTP3: no output, rebuild with ngtcp2+nghttp3+OpenSSL 3.5+ or switch to an http3-enabled image.
Does --http3 change what the server's application layer sees? No — same request/response semantics; it changes transport, not HTTP verbs. Your body/-G habits from URL and query hacks are unaffected.