linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Simulator
  • Knowledge base
  • Interview prep
Index
Categories
All entries
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
home/linux/kb/Protocols/quic-http3

kb/protocols ── Protocols ── advanced

QUIC: Modern Transport over UDP

QUIC is a transport over UDP. TLS 1.3 is built in (1 RTT, 0-RTT for resume). Multiplexing without head-of-line blocking. Connection migration (Wi-Fi to 4G without drop). HTTP/3 = HTTP semantics over QUIC.

view as markdownaka: quic, http3, h3, quic-protocol

Why QUIC

HTTP/2 solved most of HTTP/1.1's problems (multiplexing, binary framing, header compression). But HTTP/2 over TCP has a fundamental flaw: head-of-line blocking at the transport layer.

When a single packet is lost in a TCP stream, the entire stream waits for retransmit. HTTP/2 supports parallel streams at the application layer, but all of them share one TCP socket, so a lost packet blocks every stream at once.

On mobile networks this is particularly painful: high jitter, frequent packet loss. HTTP/2 on mobile is often slower than HTTP/1.1.

QUIC addresses this by moving the transport to user-space over UDP:

  • Multiplexing at the transport layer itself
  • TLS 1.3 built in (no separate handshake)
  • Connection ID instead of 4-tuple (connection survives an IP change)
  • 0-RTT resume

How QUIC differs from TCP

FeatureTCP+TLSQUIC
Transportkerneluser-space (over UDP)
TLSseparate layer (RFC 5246)built in (TLS 1.3 message in QUIC frame)
Handshake before encrypted data2-3 RTT1 RTT (or 0-RTT for known servers)
Stream multiplexingno (single stream)yes, independent streams
HOL-blockingyes (at TCP level)no (between streams)
Connection ID4-tuple (src/dst IP:port)random ID, survives IP change
Runs inOS (sendfile-friendly)user-space (higher CPU load)

QUIC vs TCP+TLS handshake

TCP+TLS 1.3:

client                 server
  │ ── SYN ────────────►│
  │ ◄── SYN/ACK ────────│        <- 1 RTT TCP
  │ ── ACK + ClientHello ─►│
  │ ◄── ServerHello + Cert │      <- 2 RTT for encrypted
  │ ── Finished + HTTP req ►│
                                  Total: ~2 RTT to data

QUIC:

client                 server
  │ ── Initial(ClientHello) UDP ──►│
  │ ◄── Initial(ServerHello+Cert) ─│   <- 1 RTT
  │ ── Handshake done + HTTP req ─►│
                                     Total: ~1 RTT

With 0-RTT (resuming a known server), data can be sent immediately with the first packet. Latency drops sharply for repeat visits.

0-RTT and its risks

On a repeated connection, the client uses a session ticket from the previous session and sends early data before the handshake is confirmed.

This is excellent for performance, but:

  • Replay attack: an attacker can capture early data and resend it. If the request changes state (POST), a replay is dangerous.
  • Server-side restriction required: only idempotent methods (GET, HEAD), or full replay protection via nonce.

In nginx:

nginx
ssl_early_data on;
# Block non-idempotent methods:
if ($request_method !~ ^(GET|HEAD)$) {
    return 405;
}

Stream multiplexing

QUIC has streams: independent bidirectional flows inside a single connection. Each stream has its own ID, its own retransmits, its own flow control.

QUIC Connection
     ├── Stream 0 (control)
     ├── Stream 4 (HTTP request 1), lost a packet, retransmitting
     ├── Stream 8 (HTTP request 2), continues normally
     └── Stream 12 (HTTP request 3), continues normally

With TCP, a single lost packet would block all of these. With QUIC, only Stream 4 is affected.

Connection migration

The connection identifier is a random byte-string in the QUIC header, not tied to an IP address. When a client switches from Wi-Fi to 4G:

  • source IP changes
  • source UDP port changes
  • Connection ID stays the same

The server sees a new IP with the same CID and silently updates the 4-tuple. The connection continues. TCP would have torn down and required a full reconnect.

Useful for:

  • Mobile applications (subway, street)
  • Long-running uploads
  • WebRTC over QUIC

QPACK: header compression without HOL

HTTP/2 uses HPACK, compression with a shared dictionary. The problem: both endpoints must know the dictionary state, otherwise decoding breaks. Over HTTP/2-over-TCP this worked (TCP guarantees order). Over QUIC it does not (streams are independent).

QPACK reworks HPACK: the dictionary lives in a separate stream with coordinated updates. More complex, but it enables header compression without HOL.

HTTP/3: HTTP semantics over QUIC

HTTP/3 describes how HTTP/2-style requests and responses are packed into QUIC streams. The semantics are the same:

  • Methods (GET/POST/...), URL, headers, body
  • Status codes (200/404/...)

Differences from HTTP/2:

  • QPACK instead of HPACK (header compression)
  • Server Push is not supported (removed from the roadmap)
  • Each HTTP request lives in its own QUIC stream

For your application the difference is minimal: the same curl https://..., the same fetch() in the browser. The transport handles everything for you.

ALPN discovery: how the client learns the server supports h3

The browser first connects via HTTPS/TCP and during the handshake receives an Alt-Svc header:

Alt-Svc: h3=":443"; ma=86400

This says "try h3 on UDP/443 next time." The browser caches it and uses QUIC on the next request.

The alternative is the HTTPS DNS RR (RFC 9460): the server declares h3 support in DNS:

example.com. 300 IN HTTPS 1 . alpn="h3,h2"

Stack support

SoftwareVersion with h3Note
nginx1.25+full support
Caddy2.6+default-on
Apachemod_http3 (exp.)in progress
HAProxy2.6+via quictls
curl7.66+ with --http3requires quiche/msh3 backend
wget22.0+yes
Cloudflare/Fastly/Cloud LByes, defaulttransparent
Chrome / Firefox / Safariyes, by defaultfeature-detection

Enabling HTTP/3 in nginx

nginx
server {
    listen 443 ssl;                          # TCP/TLS
    listen 443 quic reuseport;               # UDP/QUIC
    ssl_protocols TLSv1.3;                   # QUIC requires TLS 1.3
    ssl_certificate     /etc/ssl/fullchain.pem;
    ssl_certificate_key /etc/ssl/privkey.pem;
    add_header Alt-Svc 'h3=":443"; ma=86400';
    ssl_early_data on;                       # 0-RTT
    # ... rest is identical to TCP config
}

Do not forget to open UDP/443 in the firewall.

bash
ufw allow 443/udp
iptables -A INPUT -p udp --dport 443 -j ACCEPT

Performance

Real numbers (depend on RTT and loss rate):

  • Low RTT, low loss (datacenter): QUIC is slightly slower than TCP (CPU overhead from user-space)
  • High RTT, low loss (CDN, edge): QUIC wins due to a shorter handshake
  • High RTT, high loss (mobile network): QUIC wins significantly, no HOL blocking

Cloudflare measured: QUIC is on average 5-15% faster on page load for mobile users, up to 30% on poor networks. For cable/fiber, the difference is within statistical noise.

CPU: QUIC uses 30-50% more CPU than TCP because it runs in user-space (no kernel sendfile, no TSO/GSO until recent offload support). By 2026 many NICs and kernel versions are gaining QUIC offload (UDP-segmentation offload + TLS-offload), closing the gap.

Middlebox interference

TCP is open to middlebox inspection (headers in plaintext, sequence numbers visible). QUIC is almost entirely encrypted: only the connection ID and part of the header are in the clear. A middlebox cannot:

  • Modify packets (breaks them)
  • Track state (sees only a UDP flow)
  • DPI on content

From the ops-team perspective:

  • Firewall must pass UDP/443 (often blocked by enterprise proxies out of habit)
  • NAT tables must age UDP flows correctly (default 30 s is too short for QUIC; 5+ min is better)
  • DPI/IDS loses visibility; the security posture shifts

Troubleshooting

  • Browser not using h3: no Alt-Svc header or DNS HTTPS RR. Test with curl: curl --http3 -v https://example.com.
  • quictls fails to build: nginx requires BoringSSL/quictls (OpenSSL forks with the QUIC API). Standard OpenSSL before 3.5 does not support it.
  • UDP/443 not working: firewall (90% of cases), or the load balancer does not pass UDP. AWS ALB does not support h3; use NLB.
  • 0-RTT replay attack: see the warnings above. Never allow non-idempotent requests through 0-RTT.
  • High CPU on the QUIC server: enable kernel UDP-GSO and TLS-offload where available.
  • Connection migration not working: some middleboxes drop packets with an unfamiliar CID. Usually works transparently.
  • Old clients cannot connect: QUIC v1 is not draft-29. nginx 1.25+ supports only v1; older curl builds may expect draft-29.

When to choose QUIC

  • Mobile-heavy traffic: a real win
  • High-RTT clients (other continents): handshake advantage
  • Many small requests: multiplexing without HOL

When to skip QUIC or wait:

  • Internal RPC in a datacenter: TCP+TLS is proven; QUIC overhead is not justified
  • Long-running streams (file upload, video): TCP handles them well
  • If your stack, firewall, or observability is not ready, QUIC adds complexity

§ команды

bash
curl --http3 -v https://cloudflare-quic.com

Force HTTP/3 to verify the client and server negotiated the protocol

bash
curl -I https://example.com | grep -i alt-svc

Fetch the Alt-Svc header to check whether the server advertises h3 over h2/h1

bash
tcpdump -ni any 'udp port 443' -w quic.pcap

Capture QUIC traffic for analysis in Wireshark; decryption requires the session keys

bash
ufw allow 443/udp

Open UDP/443 for QUIC in the firewall; often forgotten after configuring nginx

bash
openssl s_client -connect example.com:443 -alpn h3

Check ALPN negotiation for h3 over TCP; not real QUIC, but validates the cert and ALPN

bash
dig +short HTTPS example.com

DNS HTTPS RR: the modern way to declare h3 support in DNS

bash
nginx -t && nginx -s reload

After adding the quic listener, validate the config and reload without downtime

§ см. также

  • http-protocolHTTP/1.1, HTTP/2, HTTP/3HTTP/1.1 is a text-based protocol with keep-alive. HTTP/2 is binary with multiplexing over a single TCP connection. HTTP/3 carries HTTP/2 semantics over QUIC/UDP without TCP head-of-line blocking.
  • http2-internalsHTTP/2: Binary Framing, HPACK, Stream MultiplexingHTTP/2 is binary multiplexing over a single TCP connection. HPACK compresses headers through an indexed dictionary. Streams are independent. Server push is deprecated. On a loss-prone link, HoL blocking is a real problem, solved by QUIC.
  • udp-basicsUDP: User Datagram ProtocolUDP delivers datagrams without establishing a connection, without retransmits, and without ordering guarantees. Header is 8 bytes. Use it for DNS, DHCP, QUIC, VoIP, and any case where latency matters more than reliability.
  • tls-handshakeTLS HandshakeTLS is the encryption layer above TCP. Before data flows, both sides run a handshake: they exchange keys, verify the certificate, and agree on a cipher.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies