⏱️ Lectura: 15 min
When you open YouTube or any site behind Cloudflare, there’s a good chance your browser is no longer using TCP to connect: it uses the QUIC protocol, which runs over UDP and reduces the initial two-round-trip handshake to a single one, or even zero on a reconnection.
📑 En este artículo
- TL;DR
- What the QUIC protocol is and why it exists
- How the QUIC protocol works internally
- The handshake: why QUIC negotiates a connection in a single round trip
- Multiplexing without head-of-line blocking
- Practical examples: checking and enabling HTTP/3
- How to get started step by step
- Real-world use cases
- Common mistakes and gotchas
- Comparison with HTTP/1.1 and HTTP/2
- Going deeper: connection migration and pluggable congestion control
- Frequently Asked Questions
- References
HTTP/3, the third version of the web protocol, relies entirely on QUIC instead of TCP. This guide explains how the QUIC protocol works internally, why the IETF standardized it, how to check whether a site already uses it, and how to enable it on your own server.
TL;DR
- You’ll understand why QUIC uses UDP instead of TCP to avoid head-of-line blocking.
- You’ll know how to tell QUIC’s 1-RTT handshake apart from the two-round-trip handshake of TCP plus TLS 1.3.
- You’ll be able to check with curl –http3 and Chrome DevTools whether a site serves HTTP/3.
- You’ll learn how to enable HTTP/3 in nginx and Caddy with the minimal necessary configuration.
- You’ll understand what connection migration is and why it helps a phone that switches from wifi to LTE.
- You’ll identify when it’s not worth forcing HTTP/3, for example behind firewalls that block UDP.
- You’ll distinguish RFC 9000 (QUIC) from RFC 9114 (HTTP/3) and what each document defines.
What the QUIC protocol is and why it exists
The QUIC protocol (Quick UDP Internet Connections) originated at Google around 2013 under the name gQUIC, as an experiment to solve a very specific problem: TCP is too rigid for the speed at which the web wants to evolve. Any change to TCP’s behavior requires updating the kernel on millions of servers, routers, load balancers, and end-user operating systems, a process that can take years to fully propagate.
QUIC avoids that problem by moving almost all of the transport logic out of the kernel. It runs over UDP, a much simpler protocol that the operating system already knows how to forward without inspecting too closely, and lets congestion control, retransmission, and encryption live in user space, inside the library or the browser. That means Chrome can improve its QUIC implementation with every release, without waiting for a kernel patch across millions of devices.
The IETF took Google’s original design, reviewed it for years in its QUIC working group, and published the final specification as RFC 9000 in May 2021. The mapping of HTTP onto QUIC was documented separately in RFC 9114, published in June 2022: that document is, literally, the definition of HTTP/3.
These two concepts should be kept clearly separate. QUIC is the transport layer, equivalent to TCP. HTTP/3 is the application layer that runs on top of it, the same way HTTP/1.1 and HTTP/2 run on top of TCP. The QUIC protocol can be used for things besides HTTP, such as DNS over QUIC, just as TCP serves applications beyond the web.
How the QUIC protocol works internally
The most visible difference between QUIC and TCP is the layer they run on. TCP is a kernel protocol of the operating system, with ordered-delivery guarantees that the kernel itself enforces. QUIC, by contrast, lives in user space and uses UDP only as raw packet transport, without inheriting any of TCP’s guarantees: UDP doesn’t order, retransmit, or acknowledge anything on its own. QUIC rebuilds those guarantees itself, but with a different design that makes it much more flexible.
The following diagram compares the protocol stack of classic HTTP/2 against that of HTTP/3 with QUIC:
flowchart TD
subgraph HTTP2["Classic HTTP/2 stack"]
A1["HTTP/2"] --> A2["TLS 1.3"]
A2 --> A3["TCP"]
A3 --> A4["IP"]
end
subgraph HTTP3["HTTP/3 stack with QUIC"]
B1["HTTP/3"] --> B2["QUIC with TLS 1.3 built in"]
B2 --> B3["UDP"]
B3 --> B4["IP"]
end
Notice that in HTTP/2, TLS encryption is a layer separate from TCP. In QUIC, TLS 1.3 is built into the transport protocol itself, not a separate layer. That integration is what lets QUIC encrypt things TCP never could, such as packet sequence numbers, leaving much less information visible to anyone watching the traffic in the middle.
Each QUIC connection can open multiple independent streams within the same UDP socket. In practice, a stream is an ordered sequence of bytes that the application uses for a specific HTTP request. The key point is that losing a packet belonging to stream 1 doesn’t block delivery of packets on stream 2 or 3, something that does happen in HTTP/2 over TCP, because TCP delivers bytes in a single global order without distinguishing which logical stream they belong to.
The handshake: why QUIC negotiates a connection in a single round trip
Opening a classic HTTPS connection over TCP takes at least two round trips before the first byte of application data travels: one for the TCP handshake (SYN, SYN-ACK, ACK) and another for the TLS 1.3 handshake (ClientHello, ServerHello with certificate, Finished). On high-latency networks, like a mobile connection to a distant server, those two round trips are noticeable every time you open a new tab.
QUIC combines transport establishment and encryption into a single exchange. The first packet the client sends already carries the embedded TLS 1.3 ClientHello; the server responds with its ServerHello, the certificate, and the transport parameters in the same flight of packets. That’s enough to achieve a 1-RTT handshake on a new connection, half the round trips of TCP plus TLS negotiated separately.
If the client has already connected to that server before and saved a previous session, it can send application data in the very first packet, even before the server confirms anything: that’s 0-RTT. The risk is that an attacker could capture and replay that first packet, so 0-RTT is only safe for idempotent requests like a GET with no side effects, never for a POST that charges money or modifies state.
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: First connection with QUIC, 1-RTT handshake
C->>S: Initial packet with ClientHello
S-->>C: Initial plus Handshake with ServerHello and certificate
C->>S: Handshake complete and HTTP/3 application data
Note over C,S: In a reconnection with 0-RTT, the data goes in the first packet
Multiplexing without head-of-line blocking
Head-of-line blocking is the most cited technical reason for HTTP/3. In HTTP/2 over TCP, if a single packet belonging to any stream is lost, TCP holds all subsequent bytes in the receiver’s buffer until that lost packet is retransmitted and acknowledged, even if those bytes belong to a completely different stream that had no loss at all.
QUIC solves this because each stream has its own sequence number space within the protocol. A lost packet only blocks the stream it belongs to; the other streams on the same connection keep delivering data to the application without waiting for any unrelated retransmission.
flowchart LR
A["A single QUIC connection"] --> B["Stream 1: image.jpg"]
A --> C["Stream 2: styles.css"]
A --> D["Stream 3: api/orders"]
B -.->|"lost packet"| E["Only stream 1 waits for retransmission"]
C --> F["Stream 2 keeps delivering data"]
D --> G["Stream 3 keeps delivering data"]
On a page with dozens of resources (images, stylesheets, API calls), this matters especially on networks with frequent packet loss, like the saturated wifi at a coffee shop or a mobile network with poor coverage, where a single loss no longer drags down the entire page.
Practical examples: checking and enabling HTTP/3
First, confirm that your curl version can negotiate the QUIC protocol, because not all builds include it:
curl -V | grep -i http3
This command lists your curl build’s capabilities; if the Features line includes HTTP3, you can force a request over QUIC directly.
💡 Tip: run curl -V first. If the protocols line doesn’t include HTTP3, your curl build can’t negotiate it even if the server supports it.
curl --http3 -o /dev/null -s -w "negotiated protocol: %{http_version}\n" https://cloudflare.com/
This command discards the response body and only prints the HTTP version the connection ended up using. If the result shows 3, the site responded over QUIC for that particular request.
On the server side, enabling HTTP/3 in nginx (starting from the 1.25 branch, which includes the ngx_http_v3_module module) means opening a UDP listener for QUIC in addition to the usual TCP listener, and announcing it with the Alt-Svc header:
server {
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
ssl_certificate /etc/ssl/certs/programacion.crt;
ssl_certificate_key /etc/ssl/private/programacion.key;
ssl_protocols TLSv1.3;
add_header Alt-Svc 'h3=":443"; ma=86400';
location / {
root /var/www/programacion;
}
}
The reuseport flag lets the kernel distribute incoming UDP packets across several nginx worker processes, which is necessary because UDP doesn’t have the concept of an accepted connection that TCP has: without reuseport, all QUIC packets would pile up on a single worker. The Alt-Svc header is what tells the browser to try QUIC on that same port next time; it’s necessary because a client’s very first visit always comes in over TCP and TLS, QUIC only kicks in starting with the second request, once the browser has seen that header.
How to get started step by step
To bring HTTP/3 to your own server, in order:
- Confirm that your web server supports QUIC: nginx 1.25 or higher, Caddy (enabled by default since version 2) or a proxy like HAProxy 2.6 or higher with the QUIC module.
- Open the corresponding UDP port, usually 443/udp, in the server’s firewall and in any cloud security group you use, in addition to the 443/tcp you already had configured.
- Configure the TLS 1.3 certificate the same way you would for normal HTTPS: QUIC reuses exactly the same certificate.
- Add the
quiclistener and theAlt-Svcheader as in the example above. - Reload the server and verify with
curl --http3or the Network tab in Chrome DevTools, Protocol column, looking for the valueh3.
With Caddy the process is shorter because HTTP/3 is enabled by default as soon as the server obtains a valid certificate via ACME; there’s no need to touch any additional flag for it to start announcing it.
Real-world use cases
Cloudflare was one of the first networks to enable HTTP/3 in production for its customers, and documented the process and motivation on its technical blog. Google uses it on YouTube and across most of its services, even from before the RFC’s final standardization, since it originated the protocol. Chrome, Firefox, Safari, and Edge have supported it natively for several versions now and negotiate it automatically when the server announces it.
Outside the browser, the QUIC protocol also serves as the foundation for DNS over QUIC (DoQ), designed to resolve names with less latency and more privacy than traditional DNS over plain UDP, and for real-time video call and streaming protocols that need low latency and tolerance for packet loss without giving up modern encryption.
Common mistakes and gotchas
The most frequent gotcha is assuming that enabling QUIC on the server is enough. Many corporate networks and some mobile carriers block outbound UDP traffic on port 443 outright, because that port traditionally only carried TCP. When that happens, the browser simply falls back to TCP and TLS without notifying the user or the site operator.
⚠️ Heads up: don’t assume HTTP/3 is working just because you configured it on the server. Many corporate firewalls block UDP/443 entirely and the browser falls back to TCP without warning; always verify with curl or DevTools from the end user’s actual network.
Another common mistake is forgetting the Alt-Svc header: without it, no browser knows it can try QUIC, because discovery depends on having seen it in a previous response served over TCP.
A third problem shows up in infrastructures with load balancers that round-robin UDP packets without understanding QUIC: since each QUIC connection is identified by a Connection ID rather than the IP-and-port tuple, a naive balancer can send packets from the same connection to different backend servers and break it completely. Modern load balancers with QUIC support read the Connection ID to maintain session affinity.
Comparison with HTTP/1.1 and HTTP/2
| Option | When to use it | Advantage | Limitation |
|---|---|---|---|
| HTTP/1.1 | Maximum compatibility with very old clients or embedded devices | Simplicity, plain text that’s easy to debug | Serial connections per origin, no real multiplexing |
| HTTP/2 | Sites with many resources that already run on existing TCP infrastructure | Stream multiplexing over a single TCP connection, server push | Head-of-line blocking at the TCP level if a packet is lost |
| HTTP/3 | Users on unstable mobile networks or sites with huge numbers of parallel resources | 1-RTT or 0-RTT handshake, truly independent streams, connection migration | Needs UDP/443 open and servers or load balancers with QUIC support |
Going deeper: connection migration and pluggable congestion control
A little-known consequence of identifying connections by Connection ID instead of by IP and port is connection migration. When your phone switches from wifi to mobile data, its IP address changes, but the QUIC Connection ID stays the same. The server recognizes it’s the same session and no new handshake is needed: the connection simply continues where it left off.
stateDiagram-v2
[*] --> ConnectionOnWifi
ConnectionOnWifi --> Migrating: the network switches from wifi to LTE
Migrating --> ConnectionOnLTE: same Connection ID, no new handshake
ConnectionOnLTE --> [*]
Another technical piece that changes completely is header compression. HTTP/2 uses HPACK, a scheme that depends on headers arriving in strict order to keep a dynamic table synchronized between client and server. That strict order is exactly what QUIC breaks on purpose, because streams are independent and can arrive in any order. That’s why HTTP/3 uses QPACK, a redesigned compression scheme that separates the dynamic table from the delivery of each stream, using a separate control stream to synchronize updates without blocking the rest of the traffic.
Living in user space, QUIC’s congestion control also doesn’t depend on the operating system kernel. Each implementation, Chrome’s, an nginx server’s, a library like quiche’s, can bring its own algorithm, whether classic CUBIC or more aggressive variants like BBR, and update it with every application release, without waiting for an operating system patch.
💭 Key point: QUIC doesn’t just change the transport, it changes who controls the transport: it goes from being an operating system decision to being an application decision.
That same flexibility has a real cost. Since each QUIC implementation handles its own packet processing in user space, CPU usage per connection tends to be higher than TCP’s, which delegates much of the work to already-optimized kernel paths. It’s an honest trade-off: less rigidity and more control in exchange for more CPU cycles per byte transmitted, something worth measuring before migrating massive amounts of traffic.
📖 Summary on Telegram: View summary
Your next step: run curl -V | grep http3 in your terminal and, if your build supports it, try curl --http3 -o /dev/null -s -w "%{http_version}\n" https://cloudflare.com/ to see the negotiated protocol live.
Frequently Asked Questions
What’s the difference between QUIC and HTTP/3?
QUIC is the transport protocol, equivalent to TCP. HTTP/3 is the application protocol that runs on top of QUIC, the same way HTTP/1.1 and HTTP/2 run on top of TCP.
Do I need to change my application code to serve HTTP/3?
No. It’s generally a configuration change on the web server or CDN; the application code keeps speaking normal HTTP without knowing which transport was used underneath.
Do all browsers support HTTP/3?
Modern browsers, such as Chrome, Firefox, Safari, and Edge, support it and negotiate it automatically when the server announces it with the Alt-Svc header.
Is QUIC more secure than TCP plus TLS separately?
QUIC encrypts more connection metadata than TCP and TLS separately, including packet numbers, which reduces what a network observer can infer about the traffic. The encryption of the content itself is still TLS 1.3 in both cases.
Why do some corporate networks block HTTP/3?
Because it runs over UDP and many firewalls only allow outbound TCP traffic on port 443. When the browser fails to negotiate QUIC, it automatically falls back to TCP and TLS without the user noticing.
Where is the official specification?
The QUIC transport is defined in RFC 9000 and the mapping of HTTP/3 onto QUIC in RFC 9114, both published by the IETF.
References
- RFC 9000: specification of the QUIC transport protocol published by the IETF in May 2021.
- RFC 9114: specification of HTTP/3 over QUIC.
- MDN: Evolution of HTTP: historical context of HTTP/1.1, HTTP/2, and HTTP/3.
- Cloudflare’s technical blog: history and adoption of HTTP/3 on its network.
- QUIC Working Group: the IETF working group that maintains the QUIC specification.
📱 Enjoying this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Imagen destacada: Foto de Ryutaro Uozumi en Unsplash
0 Comments