Skip to content
Published on

Why QUIC Gets Slower on Fast Internet — Receiver-Side CPU and the Kernel QUIC That Hasn't Arrived

Share
Authors

Introduction — First, Where QUIC Actually Won

The QUIC story usually starts with "faster than TCP" and ends with "so turn on HTTP/3." This post covers the opposite side — the conditions under which QUIC loses, and why. But to be fair, we have to start by confirming where it won. And this isn't an impression; there are measured numbers behind it.

Google's The QUIC Transport Protocol: Design and Internet-Scale Deployment, published at SIGCOMM 2017, tables a comparison between a QUIC treatment group and a TLS/TCP control group of equal size. Carrying over the averages from the paper's Table 1 and Table 2 gives us this:

Search Latency reduction — average
  Desktop  8.0%      Mobile  3.6%

Video Latency reduction — average
  Desktop  8.0%      Mobile  5.3%

Video Rebuffer Rate reduction — average
  Desktop 18.0%      Mobile 15.3%

The paper also points to where this gain comes from. About 88% of QUIC handshakes complete in 0-RTT, which makes handshake latency almost insensitive to RTT. While TCP/TLS's average handshake latency climbs linearly with RTT, QUIC's stays nearly flat. On top of that sits an improvement in loss recovery, which in the paper's own words raises overall resilience to loss and lowers latency for short connections.

Three caveats are worth keeping in mind together. First, this is Google measuring its own traffic (vendor-measured). The measured population is Google users on Chrome and YouTube, and the servers are Google's servers — not your traffic mix. Second, this is gQUIC, the predecessor of IETF QUIC, not IETF QUIC itself. Third, the paper itself admits the limits of the gain — handshake latency accounts for well under 20% of search latency and video latency, so even eliminating handshake latency entirely only produces a small overall latency reduction.

And in that same Table 1 there's a cell that doesn't get quoted much. The reduction rates for the 1st and 5th percentile of mobile search are -0.6% and -0.3%, respectively. A negative number means QUIC was slower. In other words, for mobile users who were already in the fastest conditions, QUIC was a very slight loss even in Google's own measurement. The gains are concentrated in the tail — at the 99th percentile of desktop search, the reduction is 16.7%. QUIC is a protocol that improves the average a little and the worst case a lot.

Deployment level is also no longer experimental. As of W3Techs' July 2026 tally, HTTP/3 is used by 39.9% of all websites (per W3Techs' own survey population).

But Once the Link Gets Fast, the Story Flips

The question is under what conditions these measurements were taken. The WWW 2024 paper surveys prior QUIC measurement studies and sums them up this way — the results are a mix of gains and (in some cases) losses, and most of them concentrate on low-throughput cases. So what happens when the link gets faster?

Researchers from the University of Michigan and others dug directly into this question in QUIC is not Quick Enough over Fast Internet (Zhang et al., WWW 2024). The conclusion is exactly what the title says. Carrying over the paper abstract's sentence: on fast internet, the UDP+QUIC+HTTP/3 stack suffers up to a 45.2% drop in data transfer rate versus TCP+TLS+HTTP/2, and the gap widens as the underlying bandwidth grows.

The numbers need to be attached precisely to their conditions.

  • Comparing with command-line clients (cURL, quic_client), when bandwidth is low (roughly under 600 Mbps), QUIC and HTTP/2 perform similarly. Above that, QUIC starts falling behind, widening to as much as 15.7%.
  • In browsers, the gap is bigger. In Chrome, QUIC starts lagging once bandwidth exceeds about 500 Mbps, and once it reaches 1 Gbps, it's 45.2% slower.
  • The weaker the client, the wider the gap.

The paper's Table 1 preliminary experiment on downloading a 1 GB file shows this well (average of 10 runs).

                          Download Time(s)      CPU Usage(%)
Testbed                HTTP/2   HTTP/3        HTTP/2   HTTP/3
Desktop, Ethernet        9.32    18.60(+99%)    77.5     96.9
Pixel 5, low-band 5G    37.11    78.65(+112%)  121.55   161.77
Pixel 5, mmWave 5G      30.10    63.20(+110%)  128.43   165.20

A note of caution — the desktop CPU figures were measured on the browser's network service, while the smartphone figures are for the whole browser process. Anything over 100% means multiple cores were used. So the two rows shouldn't be compared directly against each other.

The impact isn't confined to bulk transfer. DASH video delivery showed up to a 9.8% bitrate drop, and average page load time across 100 representative sites got 3.0% longer (though the tail shows gaps over 50%). The paper's observation here is sharp — this degradation hides on 4G and surfaces on 5G. When the link is slow, the link itself is the bottleneck, so QUIC's cost simply doesn't show.

Finding the Culprit — Ruling Out Suspects One by One

This is where the paper's real contribution starts. The researchers rule out the likely suspects one by one through controlled experiments.

Is UDP itself slow? No. Re-measuring with iPerf under the same network settings, both UDP and TCP fill a 1 Gbps link — UDP at 958 Mbps, TCP at 944 Mbps. UDP is actually a bit higher. The kernel's UDP data path is innocent.

Is it the server software's fault? No. Switching to Nginx-quic left HTTP/2 about the same, and QUIC was actually 18% slower.

HTTP syntax? Encryption? Parameter tuning? Client OS? None of those either. HTTP/3 has essentially the same syntactic structure as HTTP/2, both stacks used the same TLS_AES_128_GCM_SHA256 cipher, pacing and PMTU tuning gave no noticeable improvement, and macOS and Windows receivers produced the same result.

What's left is receiver-side processing. Two observations point there. First, the QUIC client receives far more packets than the HTTP/2 client does. Second, when QUIC is receiving at high speed, the delay between a data packet and its corresponding ACK is large. Measuring the average packet RTT, HTTP/2 comes in at 1.9ms while QUIC spikes to 16.2ms. The ping RTT between the two machines is only 0.23ms, so most of that delay comes not from the wire but from packet processing at the endpoint.

The paper explicitly claims this as its own contribution — it was known that userspace implementations generally hurt performance, and there had been sender-side optimization work, but pinpointing the receiver side as the more likely bottleneck was, they say, a first. It also gives the reason: servers are typically stronger than desktops, laptops, and phones. When we habitually say "QUIC eats server CPU," the side that actually hurts turns out to be the device in the user's hand.

Why Receiving Is Expensive — GRO and Userspace ACKs

Profiling the kernel and Chromium with perf turns up two root causes.

First, the kernel touches packets far too many times. Over the course of receiving one 1 GB file, netif_receive_skb was called 231K times for QUIC versus 15K times for HTTP/2 — a 15x difference. do_syscall_64 also widens, 17K versus 4K. The reason is simple — TCP bundles multiple segments together and hands them up via GRO (Generic Receive Offload), but of the QUIC implementations the paper surveyed, not a single one used UDP GRO.

The paper's Table 3 nails this down.

Configuration        Sent packets  Received packets  Time(s)
QUIC (offload on)          743K              743K     18.60
QUIC (offload off)         744K              744K     18.82
HTTP/2 (offload on)         19K               53K      9.36
HTTP/2 (offload off)       744K              744K     10.84

How to read it: with offload turned on, the packets the kernel touches for HTTP/2 drop from 744K to 53K, and the time also drops from 10.84s to 9.36s. QUIC stays essentially the same whether offload is on or off — 743K/744K, and 18.60s versus 18.82s, no real difference. QUIC gets no benefit whatsoever from GRO/GSO.

So why not just turn it on? The paper gives three structural reasons. TCP is a byte-stream model, so its payload can be freely re-packetized, but UDP's offload logic has to preserve packet boundaries. So the existing UDP GSO/GRO can only offload a run of UDP packets of identical length. QUIC, however, is multiplexed by nature — frame sizes differ across streams and get multiplexed after encryption. A run of datagrams with irregular lengths can't be handled by existing GSO/GRO. On top of that, blindly bundling UDP datagrams into one burst creates congestion-loss and fairness problems, and because QUIC has many variants, putting offload logic into NIC hardware is also complicated.

Second, ACKs are generated in userspace. Table 4 is the time breakdown of the Chromium networking stack.

Chromium networking stack stage         QUIC(8.5s)   HTTP/2(4.1s)
Read UDP/TCP packet from socket           0.248s        0.037s
Extract payload from UDP/TCP packet       0.310s        0.084s
Decrypt QUIC/TLS encrypted packet         0.660s        0.814s
Parse decrypted QUIC/HTTP2 frame          3.468s        3.182s
Generate QUIC response (ACK, etc.)        2.972s          --
Other                                     0.859s        0.001s

There are two things to read out of this table.

One is that ACK generation alone takes 2.972 seconds in its entirety. The HTTP/2 column doesn't have this line item at all — TCP's ACKs are generated by the kernel, and thanks to optimizations like delayed ACK and receive offload, they go out far more rarely and far more efficiently. QUIC does this work in userspace, per packet, without an optimization like delayed ACK. Out of a total download time of 20.6 seconds, QuicChromiumPacketReader eats 8.7 seconds, and 3.0 seconds of that is response generation.

The Surprising Part — Encryption Isn't the Culprit

Look at that table again. In the decryption stage, QUIC is 0.660s, HTTP/2 is 0.814s. QUIC is actually faster.

The common explanation "QUIC is slow because of encryption" is, at least in this measurement, not true. QUIC used TLS 1.3, HTTP/2 used TLS 1.2, and the ciphers were the same; the researchers say they also benchmarked with other cipher suites and found no meaningful effect on performance. The cost isn't in the cryptographic operations — it's in touching packets too many times, too often, crossing the user/kernel boundary.

This misconception is common enough to be worth flagging directly. Before you go reach for kernel bypass or hardware crypto acceleration, profile first to check whether your bottleneck is actually AES.

That said, symmetric crypto isn't permanently off the hook — as we'll see later, once the send path is optimized enough, encryption does climb back up the list. It's a question of ordering.

The Sender-Side Story — How Google Cut It Down to 2x

If receiver-side is a new finding, sender-side is an old battlefield. Google's Ian Swett presented QUIC CPU Performance at SIGCOMM EPIQ 2020, and the slides are a real record of that fight. The subtitle is candid — "Can HTTP/3 be as efficient as HTTP/2 and HTTP/1.1?"

Starting point: as of January 2017, QUIC's CPU cost was 2x that of HTTPS 1.1. The initial implementation was 3.5x, and it had come down to 2x through obvious fixes — removing redundant calls to expensive functions, removing allocations from the data path, minimizing copies, and workload-specific data structures.

The location of the bottleneck is also clear. Per the slides, UDP transmission accounts for 25% of this workload's CPU, exceeding 50% in some environments and benchmarks. On Linux, UDP sendmsg costs up to 3.5x the cycles/byte of TCP.

Here are the optimizations applied after that, along with their measured effects (all Google's own measurements).

UDP GSO                             7% faster than TCP GSO
UDP GSO + hardware offload          an additional 2-3x
  Cumulative speedup                ~10x ideal, typically 5x
  => worst-case 50% CPU down to 5%, ~2x improvement overall
UDP GRO (patch)                     35% improvement in receive CPU
1-STREAM-frame-per-packet optimization  5% savings on its own
Sending fewer ACKs                  25% savings
  - decisive in reaching parity with TCP in the Quicly benchmark
FDO + ThinLTO                       15% CPU savings

Two things stand out here.

First, cutting ACKs alone gives a 25% savings, and the slides state this was decisive in reaching parity with TCP in the Quicly benchmark. It points to exactly the same spot as the ACK bottleneck the WWW 2024 paper found on the receiver side. Whether sender or receiver, QUIC's ACKs are expensive.

Second, once the send path is made fast enough, symmetric crypto ends up taking about 30% of CPU. And because IETF QUIC needs 2-pass encryption for header protection, in the slides' words, small cryptographic operations are much more expensive than bulk encryption. It's a design-level trade — CPU paid for privacy.

The slides' outlook is also conditional — they say QUIC should become as fast as kTLS given symmetric-crypto offload, while at the same time leaving an open question: "Is QUIC offload worth it? TSO's benefit is mixed, especially at low bandwidth." Worth reading with the fact that this was a 2020 talk in mind.

The 2026 Attempt — Putting QUIC in the Kernel

At this point, a natural question comes up. If the problem is touching packets one at a time in userspace, why not just put it in the kernel?

That's exactly what's happening right now. Xin Long is posting a patch series implementing QUIC in the Linux kernel. LWN covered it on July 22, 2025 as QUIC for the kernel, and v2 followed as net: introduce QUIC infrastructure and core subcomponents — 15 patches, 9,081 lines added, corresponding to parts 1-2 of a planned five-part series, marked EXPERIMENTAL.

The design is laid out in the lxin/quic repository. The core idea is to leave only the handshake in userspace and put everything else in the kernel.

  • Userspace: only handling and generating the raw TLS handshake messages. A TLS library like GnuTLS is responsible, exchanging messages with the kernel via sendmsg/recvmsg, with crypto info passed through cmsg.
  • Kernel: the rest of the QUIC protocol, in full. Instead of using the ULP layer, it creates an IPPROTO_QUIC-type socket (similar to how IPPROTO_MPTCP does it), operating over a UDP tunnel.
  • Kernel consumers: the existing net/handshake netlink is used to request a handshake from userspace on the kernel's behalf.

The motivation isn't only performance. LWN points to two things. One is enabling hardware protocol offload — the GSO/GRO and crypto offload story from earlier connects here. The other is kernel consumers. There's an open pull request adding QUIC support to Samba's server and client, and there's interest in adding that support to in-kernel SMB and NFS filesystems as well. A browser can just link a userspace QUIC library, but a kernel NFS client can't do that.

Does Putting It in the Kernel Make It Faster — Reading the Self-Benchmark Honestly

So did it actually get faster? The lxin/quic repository's README publishes iperf figures measured on a 100G physical NIC. This is a project's own measurement, a synthetic benchmark, not a real workload.

First, kernel QUIC versus kTLS.

UNIT        size:1024      size:4096      size:16384     size:65536
Gbits/sec   QUIC | kTLS    QUIC | kTLS    QUIC | kTLS    QUIC | kTLS
--------------------------------------------------------------------
mtu:1500    1.67 | 2.16    3.04 | 5.04    3.49 | 7.84    3.83 | 7.95
no GSO           | 1.73         | 3.12         | 4.05         | 4.28
--------------------------------------------------------------------
mtu:9000    2.17 | 2.41    5.47 | 6.19    6.45 | 8.66    7.48 | 8.90
no GSO           | 2.30         | 5.69         | 8.66         | 8.82

On the surface it looks grim. At MTU 1500 and a 64KB message, kernel QUIC is 3.83 Gbps while kTLS is 7.95 Gbps — losing by more than double. It loses even after going into the kernel.

But look at the no GSO row. kTLS with TCP GSO turned off drops to 4.28 Gbps under the same conditions — nearly matching kernel QUIC's 3.83. At MTU 9000 it's even clearer — kTLS only drops from 8.90 to 8.82 with GSO off, and QUIC at 7.48 is already close.

In other words, the gap between kernel QUIC and kTLS is mostly GSO. The README states this explicitly too — TCP GSO contributes heavily to TCP and kTLS performance at MTU 1500 and with large messages, and once TCP GSO is turned off, the remaining small gap has three causes.

  • QUIC has one extra copy on the TX path.
  • QUIC has one extra encryption pass for header protection.
  • QUIC's stream-DATA header is longer.

The table pitting encryption-off QUIC against TCP tells the same story.

UNIT        size:1024      size:4096      size:16384     size:65536
Gbits/sec   QUIC | TCP     QUIC | TCP     QUIC | TCP     QUIC | TCP
--------------------------------------------------------------------
mtu:1500    2.17 | 2.49    3.59 | 8.36    6.09 | 15.1    6.92 | 16.2
no GSO           | 2.50         | 4.12         | 4.86         | 5.04
--------------------------------------------------------------------
mtu:9000    2.47 | 2.54    7.66 | 7.97    14.7 | 20.3    19.1 | 31.3
no GSO           | 2.51         | 8.34         | 18.3         | 22.3

At MTU 1500 and 64KB, TCP dominates at 16.2 Gbps, but with GSO off it collapses to 5.04. QUIC in the same cell is 6.92 — kernel QUIC beats TCP with GSO off.

The conclusion here isn't "the kernel is the answer." It's that offload is the answer, and going into the kernel is how you earn the right to use offload. TCP is fast not because it's in the kernel, but because it's riding on 30 years of an offload ecosystem — TSO/GSO/GRO, NIC hardware support. Putting QUIC in the kernel is the first step toward getting in line for that ecosystem, not the destination.

But It's Not in Mainline Yet

Let's be clear about the single most important fact. LWN's July 2025 article wrote that seeing this land in mainline "seems unlikely before some point in 2026 at the earliest."

I checked directly, as of today (July 16, 2026). You can check whether the kernel tree has a net/quic directory.

# mainline (torvalds/linux)
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/quic"
# => 404

# net-next (branch targeting the next merge window)
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/tree/net/quic"
# => 404

# control: net/mptcp, already merged
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/mptcp"
# => 200

It's in neither mainline nor net-next. The control, net/mptcp, returns 200, so it's not a URL-pattern problem either. Even now, with Linux 7.0 having shipped in April 2026, kernel QUIC is still out of tree. If you want to use it, you have to build the out-of-tree module yourself, the repository still carries the EXPERIMENTAL label, and only parts 1-2 of a planned five-part series have been posted.

The case LWN cites for comparison is suggestive — the Homa protocol went through 11 revisions over 9 months and still wasn't merged. Getting a new protocol into the kernel networking subsystem is inherently this slow. "Kernel QUIC is coming soon, let's wait for it" cannot be a plan.

Is That 45.2% Still Valid Today

There's one more thing worth being honest about. The WWW 2024 paper measured with Chromium v102 — a 2022 build. The implementation has moved since then.

One of the paper's recommendations was: "Chromium currently reads UDP packets one at a time with recvmsg; reading several in one syscall with recvmmsg could improve receive performance." Opening Chromium's source today, there's a comment in net/socket/udp_socket_posix.h noting that on platforms supporting recvmmsg (Linux, ChromeOS, Android), it's used. So some of the recommendations appear to have been adopted.

On the other hand, browsing that same file plus net/socket/udp_socket_posix.cc, I didn't see code turning on UDP_GRO. This is only the result of me checking two specific files at one specific point in time, and Chromium has multiple socket paths, so it's weak grounds for a firm claim — but it's a point that can be verified directly, so I'm noting it here.

So don't pin the number 45.2% onto today's Chrome as-is. What that number tells you isn't an exact multiplier but a mechanism. A userspace transport has to do work the kernel used to do for it (packet coalescing, ACK generation, delayed ACK), and the moment the link gets fast enough that the bottleneck shifts from the wire to the CPU, that cost shows up — this mechanism survives even as implementations change. The multiplier is something you have to re-measure in your own environment.

So When Should You Use It, and When Shouldn't You

Pulling together the primary sources so far, the decision criteria come out fairly sharp.

When QUIC/HTTP3 earns its keep

  • Your client is on a lossy, long-RTT network. Mobile, long-distance, unstable wireless. This is exactly why Google's measured gains were concentrated in the tail (99th percentile).
  • Connections are short and numerous. The 0-RTT/1-RTT handshake gain becomes relatively larger.
  • Connection migration is a genuine requirement. Clients moving between cellular and Wi-Fi.
  • You want to improve metrics where the worst-case segment dominates user experience, like rebuffering.

When it's overkill or a net loss

  • The link is fast and lossless. Inside a data center, gigabit wired. This is exactly the condition of the WWW 2024 paper. The moment the wire stops being the bottleneck, only QUIC's CPU cost remains.
  • Large bulk transfer is the primary workload. File downloads, backups, large artifacts.
  • The client is weak. Embedded devices, low-end Android. This is exactly where the gap was widest in the paper's Table 1.
  • You need a kernel consumer. Kernel NFS/SMB can't link a userspace QUIC library, and kernel QUIC isn't in mainline yet.

If you're already using QUIC and it's slow, work through this order. Crypto acceleration or kernel bypass comes last.

  1. Confirm the bottleneck is really CPU. If the wire is the bottleneck, QUIC's CPU cost won't show.
  2. If it's sender-side, start with UDP GSO. In Google's measurement, adding hardware offload on top of that typically gets you 5x.
  3. Look at ACK frequency. Roughly 25% is at stake on both the send and receive side.
  4. Only after that should you suspect the crypto. In the WWW 2024 measurement, decryption was actually faster for QUIC than for HTTP/2.

Closing

To sum up: QUIC genuinely won on handshake and loss recovery, and the gain is bigger in the tail than on average. And it loses once the link gets fast — the inflection point, where the bottleneck shifts from wire to CPU, sits around 500-600 Mbps, and above that the cost paid by the userspace transport shows through plainly.

The cost's real identity isn't encryption, it's the number of times packets get touched. Because the kernel isn't coalescing them via GRO, 15x more packets climb up into userspace, and the ACKs the kernel used to generate now get made by hand, with no delayed-ACK optimization. That's why 2026's answer is "put it in the kernel" — and that answer is still under review, present in neither mainline nor net-next.

The patch's own benchmark delivers the last lesson. Even in the kernel, it still loses to kTLS by nearly double. But turn off TCP's GSO, and the gap all but disappears. In the end this wasn't a question of kernel versus userspace, but of whether you can use offload. TCP is fast not because it's old, but because being old means the hardware has had time to help it.

It'll take more time before QUIC gets that same help. Until then, before you pick a protocol, measure whether your bottleneck is the wire or the CPU. The right answer flips depending on that.

References