HTTP/3 was standardised four years ago; QUIC's RFC is five years old. So this is no longer something we can call "coming soon" — it is out there, in production, across billions of requests. Which is exactly why the right question today is not "should I move to HTTP/3?" The right question is: if we moved, what changed, and if we didn't, what are we missing?
Before writing this I sent a simple request to my own site. The result was not what I expected; I'll get to it shortly. First the numbers, because the numbers don't quite match the story that gets told about this protocol.
The numbers first: HTTP/3 won, but not the way people think
According to Cloudflare's 2025 year in review, the split of requests reaching them looks like this: 50% HTTP/2, 29% HTTP/1.x, 21% HTTP/3. In that report's own words these shares are largely unchanged from 2024 — HTTP/2 and HTTP/3 each gained only fractions of a percentage point over the year.
Take that phrase "requests reaching them" seriously: this is not the whole internet, it is one CDN's traffic mix. But that is exactly where the lesson hides.
What stops me isn't HTTP/3's 21%, it's that HTTP/1.x is still at 29%. In 2026, in HTTP/2's eleventh year, close to a third of the requests arriving at one of the world's largest CDNs still flow over a protocol from 1999.
The report itself explains why, and I find that sentence more instructive than anything else in it: seven countries or regions sit below 10% HTTP/3 — Hong Kong, Dominica, Singapore, Ireland, Iran, Seychelles and Gibraltar — and Cloudflare attributes this to "high levels of bot-originated HTTP/1.x traffic."
That list isn't a backwardness table, it's a hosting-geography table. Hong Kong and Singapore land on it not because their residents run old browsers, but because they are datacentre jurisdictions.
And the other end? In 2025, 15 countries or regions sent more than a third of their requests over HTTP/3, up from eight in 2024. Georgia leads at 38%, just past Réunion's 37%, which was 2024's top rate. The largest jump of the year was Armenia's, from 25% to 37%.
The conclusion that follows is more interesting than "HTTP/3 is spreading slowly." When you think you're measuring a protocol's adoption rate, you're actually measuring who — and what — is sending requests to that network. A human's browser speaks h3; the army of bots, scripts and SDKs nobody updates does not.
The request I sent to my own site
30 August 2026, from my Mac. First the headers:
curl -sI https://mustafaerbay.com.tr/ | grep -i alt-svc
# alt-svc: h3=":443"; ma=86400
The site offers HTTP/3. It sits behind Cloudflare, and the alt-svc header says "you can also reach me over h3, remember this for 24 hours." Then I tried to actually connect over HTTP/3:
curl --http3 https://mustafaerbay.com.tr/
# curl: option --http3: the installed libcurl version doesn't support this
The curl that ships with macOS is 8.7.1, released 27 March 2024, built against LibreSSL and SecureTransport. No HTTP/3 support. When I do nothing special, the request quietly falls back to HTTP/2 and completes in 132 ms.
That is the secret behind both the 29% HTTP/1.x slice and the 50% HTTP/2 slice. The server side is ready. Browsers are ready. What isn't ready is the enormous tail of everything else: packaged curl builds, language SDKs, your payment provider's webhook, an IoT device's firmware, that internal Python script. These clients aren't rejecting HTTP/3 — they have no idea it exists.
ℹ️ Practical takeaway
When you turn on HTTP/3, measure what share of your traffic actually moved to h3. On a browser-facing site that number will be high; on an API-heavy service it can be surprisingly low. The gap between "we enabled it" and "it is being used" is the most misunderstood part of this protocol.
The most important box in that diagram isn't the one on the right. It's the one that says "silently, no error." When HTTP/3 fails, nothing raises an alarm. That is a good design decision, but operationally it means one thing: unless you measure it, you cannot know whether your HTTP/3 works at all.
The three things that genuinely changed
Three of the most-discussed features genuinely earned their keep in the field.
First, connection setup. QUIC collapses the transport and encryption handshakes into a single round trip, and with 0-RTT a client can send data in its very first packet to a server it has talked to before. There is a price, and RFC 9001 states it in two separate places. Section 5.6 says application data arriving in 0-RTT may be processed by the server more than once rather than just once, and that the resulting actions "could have unwanted consequences"; the same section defers the HTTP profile to RFC 8470. Section 9.2 puts it more bluntly: disabling 0-RTT entirely is the most effective defence against replay attack.
The rough rule of thumb gets summarised as "GET yes, POST no", and I explained it that way for years myself. Reading RFC 8470 shows that isn't enough: a GET with side effects also causes harm when replayed. That is precisely why the standard defines the 425 Too Early status code — so the server can say "I won't take this from early data, retry it on a normal connection."
Second, connection migration. QUIC binds a connection to a connection ID rather than to the IP and port four-tuple. When your phone moves from Wi-Fi to cellular, the TCP session dies while the QUIC session survives. If your audience is mobile-heavy this is HTTP/3's most concrete win — and also the hardest to measure, because the benefit shows up as an error that never happens.
Third, head-of-line blocking leaving the transport layer. In HTTP/2, one lost packet on a TCP connection stalls every stream on it. In QUIC the streams are independent. But here's the fine print: no loss, no gain. On a clean fibre link that advantage stays theoretical.
Let's be honest about speed
Cloudflare's well-known comparison of HTTP/3 against HTTP/2 is dated 14 April 2020, and its result surprises people: in real-world data HTTP/3 trailed HTTP/2 by roughly 1-4% on average in North America. In their synthetic test the first byte arrived in 176 ms versus 201 ms, a 12.4% edge for HTTP/3. The authors' own suspicion was a congestion control mismatch: HTTP/2 running on BBRv1, HTTP/3 on CUBIC.
It would be dishonest to present that figure as today's truth. It is a six-year-old snapshot taken on Cloudflare's own QUIC stack, and both congestion control practice and QUIC stacks have moved a great deal since. The lesson I take from that measurement isn't the number itself: it's that you should migrate by measuring, not by assuming HTTP/3 will be faster.
The feature that died: server push
Sometimes a spec's flashiest feature never lands. Server push is exactly that story.
Push is in the HTTP/3 RFC; once a client sends MAX_PUSH_ID, the server can push resources. Meanwhile Chrome disabled HTTP/2 push by default in version 106; on the HTTP/3 side, in the Chrome team's own words, push "was not implemented in many HTTP/3 servers and clients—even though it was included in the specification". The Chrome team's own reasoning is striking: only 1.25% of HTTP/2 sites used the feature, and that number fell to 0.7% over time. The recommended replacement is 103 Early Hints — instead of pushing the resource, telling the browser "you're going to want these, start connecting now."
A feature that exists on paper but that no major browser implements does not exist in practice. Don't build architectural decisions on top of features like that.
Prioritisation quietly moved too. HTTP/2's tree-based priority scheme gave way to the much plainer model in RFC 9218: an urgency parameter as an integer from 0 to 7 (default 3, lower means higher precedence) and a boolean flag called incremental. It travels end to end as an HTTP header and applies to both HTTP/2 and HTTP/3. If you once tried to understand the old tree and gave up, good news: there is now something understandable.
Did the cost disappear, or just move?
Moving QUIC from the kernel into user space has a price, and that price is paid in CPU. With TCP, segmentation has been handled by the kernel and the NIC for years; with UDP that work sat on the application for a long time.
Linux gained Generic Segmentation Offload for UDP in kernel 4.18, and the difference is substantial. In Cloudflare's own measurement, a transfer using plain sendmsg() issued 904,539 system calls, while the same work with GSO enabled via UDP_SEGMENT dropped to 18,824 calls. Roughly a fiftyfold reduction. Up to 64 segments can be batched at once.
Now the point that matters: in nginx this is off by default. The quic_gso directive defaults to off. Likewise quic_bpf, which routes connections to the right worker on multi-core setups, defaults to off, and so does quic_retry, which performs address validation against DDoS. Most people who enable HTTP/3 and then wonder why CPU is higher than expected never looked at those three lines.
There's one more trap, mentioned in a single sentence in the docs: when quic_host_key isn't set, it is generated randomly on every reload. So after nginx -s reload, existing session tickets and retry tokens become invalid. In an environment that deploys frequently, you want that key pinned.
So how do you actually turn it on?
Having admired that alt-svc header at the top of this article, it would be rude not to say where one comes from. The minimum nginx setup:
server {
listen 443 quic reuseport; # HTTP/3, UDP/443
listen 443 ssl; # HTTP/2 and HTTP/1.1, TCP/443
http3 on;
# Without this, nobody discovers your HTTP/3:
add_header Alt-Svc 'h3=":443"; ma=86400' always;
}
That last line is the critical one and the most commonly skipped: nginx does not emit the Alt-Svc header for you. If you don't add it yourself, your HTTP/3 technically works but no browser ever learns it exists; everyone quietly stays on HTTP/2 and you conclude "we enabled it but nobody uses it."
One more detail: QUIC requires that at least 1200-byte datagrams survive the path. When a tunnel, a VPN or an aggressive MTU setting pushes you below that threshold, the handshake fails silently. And reuseport isn't decorative either — on a multi-core box it is what spreads UDP sockets across workers.
On the discovery side, Alt-Svc isn't the only route. Browsers increasingly learn about HTTP/3 from the DNS HTTPS (SVCB) record, which is faster than Alt-Svc because it skips the TCP round trip entirely. That's why the diagram above draws the worst case: if you also set up the DNS side, the client lands straight in the box on the right.
The operational reality: nginx still says "experimental"
nginx announced HTTP/3 support with version 1.25.0 on 23 May 2023. More than three years have passed, the version has reached 1.31.4 (19 August 2026), and the sentence in the module documentation is still sitting in the same place: "The module is experimental, caveat emptor applies."
Don't treat that as a stale disclaimer. From nginx's own changelog over the past three months:
- 1.31.2 (17 June 2026): use-after-free in HTTP/3 processing — CVE-2026-42530.
- 1.31.3 (15 July 2026): IPv6 fragmentation might not be disabled when using QUIC on some operating systems.
- 1.31.4 (19 August 2026): bugfixes in HTTP/2, HTTP/3 and two other modules.
So the HTTP/3 stack is still a surface receiving active security patches. That doesn't mean "don't use it"; it means "apply its patches without delay and count your attack surface accordingly." I covered what changes on the visibility and inspection side when you open UDP/443 on a corporate network in QUIC / HTTP/3: Security and Operations on Corporate Networks; this article is its sequel on the performance and adoption side.
What we don't have yet
Three things I hear about often aren't actually ready to use yet.
Multipath QUIC. The idea of one connection using Wi-Fi and cellular simultaneously is very attractive. At the IETF it is draft-ietf-quic-multipath, now at its 21st revision, and it has reached the RFC Editor queue: it cleared the IESG and is waiting for a number. So it isn't "never coming", it's "about to arrive" — though even once published, broad implementation support is a separate matter altogether.
qlog. A standard debugging log format for QUIC, promising a consistent debugging experience across implementations. Also still a draft.
QUIC v2. The most misunderstood of the three, purely because of its name. RFC 9369 was published in May 2023 and brings no new features — it says so outright: version 2 provides no change from version 1 in the capabilities available to applications. Its purpose is to exercise the version negotiation mechanism against middlebox ossification. In other words, there is no "let's move to v2" agenda, and there shouldn't be.
Don't enable it without measuring
HTTP/3's silent fallback makes it extremely forgiving: when it breaks, users see no error and neither do you. Traffic slides to HTTP/2, pages load, everyone is happy. Months later, when you ask "weren't we on HTTP/3?", you won't have the data to answer.
The remedy is simple. Log the protocol version in your access log:
log_format h3 '$remote_addr $status $server_protocol '
'$request_time "$http_user_agent"';
access_log /var/log/nginx/access.log h3;
Now the $server_protocol field will tell you HTTP/3.0, HTTP/2.0 or HTTP/1.1. After collecting a week's worth, a simple count exposes the gap between your assumption and reality:
awk '{print $3}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
But let's be honest: that recipe measures the origin leg. This article's thesis was about the user-facing leg, and a server log cannot see it. For that you have to measure from the browser:
performance.getEntriesByType('navigation')[0].nextHopProtocol
// "h3" | "h2" | "http/1.1"
Add that one line to your RUM data and you learn directly what share of your real users speak h3 — independently of what the CDN reports. Adding it taught me more than weeks of guessing about the topic did.
If you sit behind a CDN you need to read the table in two layers: the leg between user and CDN and the leg between CDN and your origin can speak different protocols, and usually do. My site offers h3 on the user-facing side; what happens on the origin side has nothing to do with that. A graph read without making this distinction will lead you to a conclusion that is either better or worse than the truth.
Finally, the reason for a low h3 share may not be your server at all but a firewall along the path — UDP/443 is frequently blocked on corporate networks. I covered that side in detail in the earlier post.
A decision framework
Answer these five questions about your own setup, in order:
- Where does my traffic come from? If it's browser-heavy, HTTP/3 pays. If it's server-to-server API traffic, your client libraries probably don't speak h3 — verify that first.
- Are my users on lossy networks? Mobile, satellite, crowded Wi-Fi means the gain is real. A clean link between data centres means it's close to zero.
- Can I measure it? If your access log doesn't record the protocol version, enabling HTTP/3 becomes an article of faith.
- Have I reviewed the GSO, BPF and retry settings? The defaults are off, and your CPU cost depends largely on those three lines.
- Do I have patch discipline? If you run a module labelled experimental that receives regular CVEs, your update schedule has to match.
Conclusion
HTTP/3's story isn't a failure story, but it isn't a triumph either. The protocol works: it shortens connection setup and keeps sessions alive when the network changes. It genuinely helps on lossy links too. In exchange, four years after standardisation its share of requests reaching Cloudflare has stalled at one fifth, its flashiest feature retired without ever being implemented, and it still carries an experimental label in the most widely deployed web server.
I think the real lesson here isn't about the protocol. Publishing a standard isn't even the beginning of its adoption — the bottleneck is always the slowest client, the oldest library, the firmware nobody updates. Before you mark something in your infrastructure as "migrated", look at what fraction of your traffic actually flows through it. The answer is usually smaller than you hoped.
Top comments (0)