Skip to content
NexiferLabs
All services
Solutions overview
Browse the library
About Nexifer
quichttp3networkingperformanceweb-standards

QUIC and HTTP/3: what it fixes, what it costs, and where it loses

How QUIC works, why HTTP/3 wins on mobile and loses on fast fixed lines, how to deploy it on nginx or a CDN, and what to measure afterwards.

T

team

17 min read
A stylised illustration of a network, with a QUIC packet and an HTTP/3 packet travelling through it. The QUIC packet is represented as a blue circle, and the HTTP/3 packet is represented as a green circle. The network is surrounded by a network of gears and pipes, representing the infrastructure that supports it.

HTTP/3 moves the web off TCP and onto QUIC, a transport built on UDP with encryption welded into the handshake. It fixes head-of-line blocking properly, survives a phone switching from Wi-Fi to cellular mid-request, and shaves a round trip off connection setup. It is also, on a fast wired connection, measurably slower than the HTTP/2 stack it replaces - for reasons that have nothing to do with the protocol design.

Both of those things are true, and most writing about HTTP/3 only tells you the first. This piece covers how QUIC actually works, what it genuinely improves, the receiver-side CPU problem that has stalled its adoption curve, how to deploy it, and how to tell whether it helped you.

The problem QUIC was built to solve

HTTP/2 introduced multiplexing: many concurrent requests over one connection, no more queueing six at a time. It worked, and it moved the bottleneck rather than removing it.

Head-of-line blocking, one layer down

TCP delivers a single ordered byte stream. It does not know that your connection carries twelve independent HTTP requests - it sees one sequence of bytes that must arrive in order. Lose one packet belonging to a CSS file, and TCP holds back everything received after it, including complete packets belonging to a JavaScript file that has nothing to do with the loss.

LayerHTTP/1.1HTTP/2HTTP/3
Application multiplexingNone - one request per connectionYesYes
Transport awareness of streamsN/ANone - TCP sees one byte streamQUIC tracks streams itself
Effect of one lost packetBlocks that connectionBlocks every stream on the connectionBlocks only the affected stream
HTTP/2 solved application-layer head-of-line blocking and inherited the transport-layer version. That inheritance is what QUIC exists to break.

Handshakes cost round trips

Opening a TLS-secured HTTP/2 connection means a TCP three-way handshake, then a TLS handshake, then your request. That is typically two to three round trips before a single byte of application data moves. On a 200 ms mobile link, you have spent half a second on introductions.

Connections are pinned to an IP address

A TCP connection is identified by the four-tuple of source IP, source port, destination IP and destination port. Change any of them and it is a different connection. When a phone walks out of Wi-Fi range onto cellular, the source IP changes, every open connection dies, and everything reconnects from scratch.

The middlebox problem

TCP's headers are unencrypted, so three decades of firewalls, load balancers, NAT boxes and traffic shapers have been built to read and modify them. That ossification means changing TCP itself is effectively impossible: deploy a new TCP option and some middlebox somewhere will drop your packets. This is why QUIC was built on UDP rather than as TCP v2 - not because UDP is better, but because it is the only transport the internet will let you innovate on.

How QUIC works

QUIC is a transport protocol that runs in user space on top of UDP. It provides what TCP provides - reliability, ordering within a stream, congestion control, flow control - plus encryption, plus stream multiplexing that the transport actually understands.

text
HTTP/2 stack              HTTP/3 stack
─────────────────         ─────────────────
  HTTP/2                    HTTP/3
  TLS 1.3                   QUIC  (streams, reliability, TLS 1.3 built in)
  TCP                       UDP
  IP                        IP

Kernel handles TCP.       User space handles QUIC.
That last line explains most of this article.

Streams are first-class

A QUIC connection carries many independent streams, each with its own delivery guarantees. Loss on stream 4 delays stream 4. Streams 0 through 3 keep flowing. This is the head-of-line fix, and it only works because the transport itself knows the streams exist.

  • Streams are identified by a 62-bit variable-length integer, so you will not run out.
  • The low bits of the ID encode whether the stream is client- or server-initiated, and whether it is bidirectional or unidirectional.
  • Flow control operates per stream and per connection, so one greedy stream cannot starve the rest.
  • A stream can be reset independently - cancelling one request does not disturb the others.

The handshake is one round trip

TLS 1.3 is not layered on top of QUIC; it is integrated into it. The cryptographic handshake and the transport handshake happen in the same flight of packets, so a new connection reaches application data in one round trip rather than two or three.

text
TCP + TLS 1.3 (HTTP/2)          QUIC (HTTP/3)
──────────────────────          ─────────────
SYN            →                Initial + ClientHello  →
         ←  SYN-ACK                        ←  Initial + ServerHello + …
ACK + ClientHello →             HTTP request  →
         ←  ServerHello …
HTTP request   →

~2–3 RTT to first byte          ~1 RTT to first byte
Simplified. The saving is one to two round trips, which is negligible on a LAN and substantial on a 200 ms mobile link.

0-RTT resumption, and its sharp edge

On a repeat connection to a server you have spoken to before, the client can send application data in its very first packet using a cached key. Zero round trips before the request goes out.

Connection IDs, and why your phone stops stalling

A QUIC connection is identified by a Connection ID carried in the packet header, not by the IP four-tuple. When the client's address changes, it keeps sending packets with the same Connection ID and the server recognises the connection and continues. No new handshake, no dropped requests, no spinner.

This is the single most underrated feature of QUIC, and the one your users will actually feel. Walking out of a café onto the street mid-upload used to mean starting over. Endpoints also rotate through a pool of Connection IDs precisely so that observers on the path cannot use the ID to track a device across networks.

Everything is encrypted, including the transport

TCP exposes its sequence numbers, flags and options to anything on the path. QUIC encrypts almost the entire packet, including most of the header. Only the bare minimum a router needs stays visible.

Two consequences follow. Middleboxes cannot ossify what they cannot read, so QUIC can evolve in a way TCP cannot. And your existing network appliances - the ones doing deep packet inspection, traffic shaping and protocol-aware monitoring - go blind. Whether that is a feature or a problem depends on which side of the firewall you sit.

QPACK, and a deliberate compromise

HTTP/2 compressed headers with HPACK, which relies on a shared dynamic table updated in strict order - an assumption QUIC's out-of-order streams break. QPACK is the redesign: it keeps most of the compression while tolerating streams arriving in any order, at the cost of some encoder complexity and occasional blocking when a reference outruns its definition.

Amplification protection

Any UDP protocol risks becoming a reflection attack vector: spoof a victim's source address, send a small request, and the server floods the victim with a large response. QUIC's answer is a hard rule - a server must not send more than three times the bytes it has received from an address it has not yet validated. The DNS and NTP amplification problems were designed out from the start.

The part most articles leave out

QUIC is better on paper and better on bad networks. On a fast, low-loss wired connection, the measured result is often worse than HTTP/2 - and the gap widens as bandwidth increases.

The measurement

The ACM Web Conference 2024 paper *QUIC is not Quick Enough over Fast Internet* is the reference here. Across Chrome, Edge, Firefox and Opera, on desktop and mobile, over both wired broadband and cellular, the authors measured the UDP + QUIC + HTTP/3 stack delivering up to 45.2% lower data rate than TCP + TLS + HTTP/2 on high-bandwidth links. Video streaming showed up to 9.8% bitrate reduction; page load times were around 3% longer.

Why - and it is not the protocol's fault

The root cause is receiver-side CPU, and it comes from QUIC living in user space while TCP lives in the kernel with two decades of hardware and kernel optimisation behind it.

MechanismTCPQUIC
Packet coalescing on receiveGeneric Receive Offload, widely deployed, often in the NICUDP GRO exists but was not used by the implementations measured
Packets crossing into the stack~15K netif_receive_skb calls for the measured download~231K calls for the same download
Acknowledgement generationIn-kernel, with delayed-ACK batchingUser space, per-packet, no equivalent batching
CPU spent on ACKs aloneNegligibleRoughly 3 seconds of wall clock in the paper's 1 GB test
Sending many packets at onceSegmentation offload, maturesendmmsg on Linux; no public equivalent on macOS
Every row is an implementation and platform gap rather than a flaw in RFC 9000. That distinction matters for the fix, and not at all for your users today.

This is fixable. UDP GRO can be enabled and is being enabled. ACK batching can be implemented. Receive processing can be spread across cores. But the fixes are not universally deployed as of 2026, and until they are, the honest statement is that HTTP/3's benefit depends heavily on your users' network conditions.

Which explains the plateau

HTTP/3 adoption climbed quickly to roughly a third of traffic and then flattened. The usual explanation is operational inertia. The more likely explanation is that the protocol wins clearly on lossy, high-latency, mobile networks and wins ambiguously or loses on fast fixed-line connections - so the aggregate benefit stopped compounding once the easy wins were taken.

Adoption also skews toward mobile-first markets - Italy, Brazil and India lead in several measurements - which is exactly what you would expect if the benefit tracks network quality rather than fashion.

Where HTTP/3 genuinely wins

ConditionWhy QUIC helps
High packet lossLoss affects one stream instead of stalling the whole connection
High latencyOne fewer round trip on connect, zero on resumption
Mobile and changing networksConnection migration survives a Wi-Fi to cellular handover
Many small resourcesTrue multiplexing without transport-layer coupling between them
Repeat visitors0-RTT resumption for idempotent requests
Long-lived connectionsMigration and stream independence compound over the connection's life
Restrictive or lossy networks abroadAggregate effect of all of the above on the worst connections you serve

Notice the shape of that list. Every entry describes a network that is imperfect. If your users are on fibre in a major city, the honest expected gain is small. If a meaningful share are on mobile data in Dhaka, Jakarta or São Paulo, it is not.

Deploying it

The easy path

If you are behind Cloudflare, Fastly, CloudFront or most other major CDNs, HTTP/3 is either already on or is a toggle. The CDN terminates QUIC at the edge and speaks whatever your origin speaks. For most sites this is the entire deployment.

nginx

/etc/nginx/conf.d/example.conf
server {
    # HTTP/2 over TCP, kept as the fallback
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    # HTTP/3 over QUIC on UDP 443
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;

    ssl_certificate     /etc/ssl/example.com.pem;
    ssl_certificate_key /etc/ssl/example.com.key;
    ssl_protocols       TLSv1.3;

    # Tells clients arriving over TCP that HTTP/3 is available
    add_header Alt-Svc 'h3=":443"; ma=86400' always;

    location / {
        proxy_pass http://app_upstream;
    }
}
reuseport should appear on only one listen quic directive per address and port across your whole configuration.

Requires nginx 1.25.0 or later built with QUIC support. Verify before you edit anything: nginx -V 2>&1 | grep -o with-http_v3_module.

Caddy

caddyfile
example.com {
    reverse_proxy localhost:3000
}
That is the whole configuration. Caddy serves HTTP/3 by default when TLS is active.

The firewall rule people forget

bash
# QUIC needs UDP 443 open in both directions
sudo ufw allow 443/udp

# AWS security group
aws ec2 authorize-security-group-ingress \
  --group-id sg-xxxxxxxx \
  --protocol udp --port 443 --cidr 0.0.0.0/0
Blocked UDP 443 does not break your site - clients silently fall back to HTTP/2. It just means your HTTP/3 deployment quietly does nothing.

Discovery: Alt-Svc and HTTPS records

A browser's first connection to you is over TCP, because it has no way to know you speak QUIC. The Alt-Svc response header tells it, and it uses HTTP/3 from the next request onward. That means the very first page load never benefits.

DNS HTTPS records (also called SVCB) close that gap by advertising HTTP/3 support during resolution, so even a first-time visitor can go straight to QUIC.

text
example.com. 3600 IN HTTPS 1 . alpn="h3,h2" port=443
Cloudflare, Route 53 and most managed DNS providers support HTTPS records. This is the difference between HTTP/3 on the first visit and on the second.

Load balancers and proxies

This is where self-hosted deployments actually get difficult. A conventional L7 load balancer cannot inspect QUIC, because the headers are encrypted. You need a proxy that terminates QUIC itself - recent Envoy and HAProxy both do - or you terminate at the CDN and speak HTTP/1.1 or HTTP/2 to your origin, which is what most people end up doing.

Verifying it actually works

bash
# curl, if built with HTTP/3 support
curl -I --http3 https://example.com
curl -sI --http3 https://example.com | head -1   # expect: HTTP/3 200

# check the Alt-Svc header is being sent over TCP
curl -sI https://example.com | grep -i alt-svc

# confirm UDP 443 is reachable from outside
nc -zvu example.com 443

# check for a DNS HTTPS record
dig example.com HTTPS +short
curl --http3 needs a build linked against a QUIC-capable TLS library. curl --version will list HTTP3 among its features if so.

In the browser, open DevTools, go to the Network panel, right-click the column headers and enable Protocol. h3 means QUIC. Expect the first request of a cold visit to show h2 and subsequent ones to show h3 unless you have published an HTTPS record.

What to log

nginx
log_format quiclog '$remote_addr $server_protocol $status '
                   '$request_time $body_bytes_sent "$request"';

access_log /var/log/nginx/access.log quiclog;
$server_protocol reports HTTP/3.0 for QUIC requests. Split your latency percentiles by protocol - aggregate numbers will hide whichever effect is real.
  • Track the ratio of HTTP/3 to HTTP/2 requests. A sudden drop usually means a network path started blocking UDP.
  • Compare p50, p95 and p99 latency by protocol rather than in aggregate. The whole point is that HTTP/3's benefit is unevenly distributed.
  • Segment by client type. Mobile and desktop should show different results - if they do not, something is wrong with your measurement.
  • Watch CPU on your edge servers. QUIC's encryption and packet handling cost more than TCP's, which is the trade you are making.
  • Keep HTTP/2 enabled indefinitely. HTTP/3 is additive, and fallback is a feature.

What QUIC enables beyond page loads

The transport is more interesting than the HTTP mapping on top of it, and this is where the next few years of work actually are.

TechnologyStandardWhat it does
QUIC DatagramsRFC 9221Unreliable delivery inside a reliable connection - real-time media without a second socket
WebTransportW3C, over HTTP/3Bidirectional streams and datagrams from the browser, with none of WebRTC's signalling overhead
Capsule ProtocolRFC 9297The framing that lets tunnelled protocols share an HTTP/3 stream
CONNECT-UDPRFC 9298Proxy arbitrary UDP through HTTP/3
CONNECT-IPRFC 9484Tunnel whole IP packets - VPN behaviour over standard HTTP
DNS over QUICRFC 9250Encrypted DNS without TCP's handshake cost
QUIC version 2RFC 9369Near-identical to version 1, published to exercise version negotiation and fight ossification
Multipath QUICWorking Group draftOne connection across Wi-Fi and cellular simultaneously - still a draft, not yet an RFC

WebTransport is the piece most application developers should care about. It gives you reliable streams and unreliable datagrams from JavaScript over the same QUIC connection your page already uses - the right primitive for multiplayer state, live telemetry and low-latency media, and far less machinery than WebRTC.

ts
const transport = new WebTransport('https://example.com/session')
await transport.ready

// Unreliable, unordered - the right choice for positional updates
const writer = transport.datagrams.writable.getWriter()
await writer.write(new TextEncoder().encode('{"x":12,"y":40}'))

// Reliable, ordered - for anything that must arrive
const stream = await transport.createBidirectionalStream()
const streamWriter = stream.writable.getWriter()
await streamWriter.write(new TextEncoder().encode('join:room-7'))
Both delivery modes share one connection, one handshake and one congestion controller. Browser support is good but not universal - feature-detect and fall back to WebSocket.

The costs

CPU

Expect meaningfully higher CPU per request than TCP with TLS - commonly cited estimates land in the region of 10–20% more, and the measured overhead is worse on high-bandwidth transfers. TCP benefits from NIC offload and kernel optimisation that QUIC's user-space implementations are still catching up to. At CDN scale this is a real hardware line item.

Observability regresses

Your packet captures become opaque. Wireshark can decode QUIC only if you export TLS session keys, which is fine on a laptop and awkward in production. Network appliances that understood TCP see encrypted UDP. Plan for qlog - the structured logging format most QUIC libraries emit - rather than assuming your existing tooling will carry over.

bash
# Wireshark can decrypt QUIC if the client exports its keys
export SSLKEYLOGFILE=/tmp/keys.log
google-chrome --user-data-dir=/tmp/chrome-quic

# then in Wireshark:
# Preferences → Protocols → TLS → (Pre)-Master-Secret log filename

UDP is treated as second-class

Corporate firewalls, some mobile carriers and plenty of public Wi-Fi block or throttle UDP 443 on the reasonable-sounding grounds that legitimate web traffic uses TCP. Clients fall back cleanly, so nothing breaks. It does mean your HTTP/3 numbers will be lower than your configuration implies, and that the users on the most restrictive networks - often the ones who would benefit most - get it least.

Operational maturity

TCP has forty years of accumulated operational knowledge, tooling and tuning folklore. QUIC has a few. When something behaves strangely at 3am, the search results are thinner and the people who have seen it before are fewer.

0-RTT needs application awareness

Enabling ssl_early_data at the server is a one-line change. Making it safe requires your application to check the Early-Data header and refuse non-idempotent requests. Skipping the second half is how you turn a latency optimisation into a duplicated payment.

Should you enable it?

Yes, without much thought, when

  • You are behind a CDN that offers it. It is a toggle and the fallback is safe.
  • A meaningful share of your users are on mobile networks or in regions with poor connectivity.
  • Your product involves long-lived connections where migration matters - uploads, streaming, real-time collaboration.
  • You care about Core Web Vitals and your users are not all on fibre.

Think harder when

  • You self-host and your load balancer cannot terminate QUIC. That is a real infrastructure project, not a config change.
  • Your traffic is overwhelmingly high-bandwidth fixed-line, where the measured benefit may be negative.
  • You are CPU-constrained at the edge and running close to capacity.
  • Your compliance or security posture depends on deep packet inspection that QUIC will blind.
  • Your service is internal, on a fast reliable network, where none of QUIC's advantages apply.

Never

  • Disable HTTP/2 when you enable HTTP/3. They coexist by design, and the fallback path is what makes QUIC deployable at all.
  • Enable 0-RTT without handling the Early-Data header in your application.
  • Quote a supported-websites adoption figure as though it were a traffic figure.

Verdict

QUIC is a genuinely better transport than TCP for the internet most people actually use - lossy, high-latency, mobile, changing networks. Connection migration alone justifies it for anything a phone talks to. The head-of-line fix is real. The handshake saving is real. The encryption-by-default is the right default.

What it is not is a free upgrade that makes everything faster. On fast fixed-line connections the current implementations lose to HTTP/2, because TCP has decades of kernel and NIC optimisation that user-space QUIC has not finished replicating. That gap is an engineering problem with known fixes, and it is not fixed yet.

Enable it, keep HTTP/2, and measure your own percentiles split by protocol. Anyone quoting you a single speed-up number has not looked at their own data.

Our standing advice on HTTP/3

For most teams the decision is easy because the CDN makes it for you, the fallback is safe, and the users who benefit most are the ones you can least afford to serve badly. For anyone self-hosting at scale, it is a real project with a real CPU bill and an observability cost - worth doing, worth planning, not worth pretending is trivial.

Sources

Back to Blog
Share:

Related Posts