One Round Trip, Five Bytes: An HTTP/3 QUIC Handshake Anatomy Field Guide

HTTP/3 QUIC handshake anatomy field guide cover

A QUIC handshake is one round trip plus a CRYPTO frame inside it, and the byte you see at position 17 of the long header tells you whether the server will ask you to Retry, resume, or just send the request. That is the whole game. Everything else – the ACK-everything-everywhere, the stream multiplexing, the connection migration – is layer cake on top of one CRYPTO frame that rides next to a TLS 1.3 handshake in a UDP datagram. The hardest part of HTTP/3 is not the protocol; it is forcing yourself to read the packets as a sequence of small typed payloads instead of one big TCP-ish stream.

This is a field guide to the HTTP/3 over QUIC handshake, written for SREs and protocol engineers who have to debug one of these things at 2 a.m. We will walk through a fresh 1-RTT connection, a session-ticket resumption, a 0-RTT early-data flight, and the Retry packet, then compare the whole thing against TCP + TLS 1.3. The HTTP/3 QUIC Handshake Anatomy tool at Elysia Tools renders the sequence diagrams and byte-level field maps we’re going to reference inline, so you can flip between the article and the visualizer and label the same packets from both sides.

Why HTTP/3 changed the shape of “the handshake”

The TLS 1.3 spec removed the static RSA key exchange and required (EC)DHE forward secrecy, which already cut the TLS handshake from two round trips to one. QUIC, defined in RFC 9000, took that one round trip and folded it into the transport itself. The CRYPTO frame carries the TLS 1.3 handshake bytes, but QUIC’s Initial, Handshake, and 1-RTT packet types and its ACK frame replace TCP’s SYN/SYN-ACK/ACK state machine. There is no separate “TLS layer” on the wire; the handshake is just bytes inside CRYPTO frames.

The practical consequence is that you can send the first HTTP/3 request in the same round trip that completes the TLS handshake. Drop a server in Tokyo with a 200 ms RTT, and your first request still goes out at 200 ms. With TCP + TLS 1.3 over a single round trip, you would also get 200 ms. The performance argument for QUIC is not 1-RTT; it is the combination of 1-RTT, no head-of-line blocking on streams, and connection migration that survives a network change. The handshake is just where the protocol starts to show you what it is.

The fresh 1-RTT handshake, packet by packet

Five bytes that decide the HTTP/3 QUIC handshake

The first flight is a single Initial packet from the client. It carries a CRYPTO frame containing the ClientHello, a 0-RTT packet (omitted when no session ticket exists), and finally the Handshake packet from the server with the ServerHello, EncryptedExtensions, Certificate, and CertificateVerify plus the Finished. The client then sends its own Handshake packet with the client Finished. The first 1-RTT packet from the client carries the actual HTTP/3 request, and the server’s response rides in the same packet type.

The byte-level layout of the Initial (long-header) packet is what the HTTP/3 QUIC Handshake Anatomy detail mode shows you. The first byte is the header form (1) plus a fixed bit, plus the long-header type (00 for Initial), plus the packet-number length. The next four bytes are the version: 0x00000001 for QUIC v1. The next 1-to-20 bytes are the DCID length followed by the DCID; then 1-to-20 bytes for the SCID length and SCID; then a variable-length token length (zero for a fresh handshake); then a variable-length packet length; then the packet number; then the CRYPTO frames. Anyone pretending this is “just TLS over UDP” should try to type out the first 22 bytes by hand.

The reason this matters in practice is that almost every modern QUIC stack has a flag byte reserved for the Retry packet. If the server wants to do address validation, it sends a Retry packet – long-header type 11 – with a token. The client then re-sends the Initial with the token appended, and the server uses AEAD to verify the token’s integrity. Retry never carries application data, which is what the token machinery guarantees: a spoofed client cannot get a server to burn resources on a CRYPTO frame if the token does not validate.

Session resumption and 0-RTT, the hard part

Resumption is the moment when HTTP/3 stops looking like a slow handshake and starts looking like a magic trick. After a successful connection, the server hands the client a NewSessionTicket in a 1-RTT packet. On the next connection, the client slips the ticket into the ClientHello’s PSK extension and the server’s first Handshake packet contains a server Finished that confirms the PSK. The new connection still involves a CRYPTO handshake, but the bulk of the server’s heavy lifting (the certificate chain) disappears.

0-RTT is layered on top of that. The client takes data it assumes the server will accept (the PSK identity plus a client Finished bound to early-data keys) and pushes it onto the wire in the first flight. The server replies with an early-data indication in the EncryptedExtensions, and then the application data is in flight before the CRYPTO handshake would normally complete. The replay risk is real, and the standard defense is that 0-RTT data must be idempotent on the server side. This is also why 0-RTT is conditional: the HTTP/3 QUIC Handshake Anatomy 0-RTT scenario explicitly marks the early-data flight as a “replay risk” you have to engineer around, not a free win.

A second nuance: when you accept a 0-RTT connection, the cryptographic keys you derive are not the same as the resumption keys. The early-data traffic uses the early-data secret; only after the server Finished do you have the 1-RTT keys for the rest of the connection. If you mix the two (say, by trying to encrypt your first request before the server Finished reaches you), you have a bug.

QUIC vs TCP + TLS 1.3, the comparison table your platform team will ask for

QUIC vs TCP + TLS 1.3 comparison by the numbers

QUIC’s handshake is one round trip, integrated with the transport, and immune to the SYN/SYN-ACK/ACK dance plus the SYN flood amplification that TCP exposes. TCP + TLS 1.3 is also one round trip, but the TCP handshake still has its own state machine and its own retransmission rules, and TCP’s three-way handshake is what stateful firewalls and DDoS scrubbers actually see. QUIC’s Initial packets include a server Retry option for address validation, which mitigates the same amplification problem at the QUIC layer instead of relying on TCP SYN cookies.

Time to first byte is the same for the two over a clean network. What diverges is what happens after the first byte. QUIC’s 1-RTT ACK is per-packet, and lost packets only block the streams that had data in them; TCP’s head-of-line blocking is per-connection, which means one lost segment stops every stream sharing the connection. Connection migration is the second big difference: QUIC connections are identified by a 64-bit connection ID, so the client can move from Wi-Fi to cellular without re-handshaking. TCP, by contrast, is identified by the four-tuple (source IP, source port, destination IP, destination port), so any change breaks the connection. The middlebox maturity gap is real; some corporate networks still only allow TCP/80 and TCP/443, and HTTP/3’s UDP/443 traffic has to be carried as a fallback.

When to use the HTTP/3 QUIC Handshake Anatomy visualizer

The visualizer is the right tool when you are reading a wireshark capture and want to map a sequence of packets back to the Flight 1 / Flight 2 / Flight 3 vocabulary of the RFC. Pick the scenario first – fresh, resumed, or 0-RTT – then turn on Detailed mode to see byte-level field layouts for every long-header packet. The Retry packet map is worth keeping open in a separate tab during a debugging session, because address-validation failures look like a perfectly normal Initial packet until you flip the byte map and the token field is empty.

For learners, the best exercise is to flip between the overview and detailed views of the same fresh-1-RTT scenario and try to draw the CRYPTO frame boundary yourself. The visualizer keeps the flights grouped by round trip, which matches the way the RFC frames the handshake. For interviewers, the side-by-side comparison against TCP + TLS 1.3 is the cheat sheet you need to answer the “why not just keep TLS 1.3 over TCP” question without sounding like a blog post.

How to read the visualizer output before you trust it

A few details to verify before you ship a debugging session’s worth of diagrams. First, the CRYPTO frame boundaries: each CRYPTO frame starts with a varint offset and length, and the visualizer should label offset 0 as the start of the ClientHello. Second, the packet-number length: an Initial packet with a 1-byte packet number and a 1200-byte payload is a normal client; a 4-byte packet number on an Initial is unusual and worth a second look. Third, the ALPN: the visualizer should show alpn: h3 on the server’s EncryptedExtensions; if you see http/1.1 or h2, you are not looking at HTTP/3.

One more thing the tool can do: the “show comparison” toggle overlays the TCP + TLS 1.3 reference on the same diagram, which is useful when you are writing a runbook and need to show the team where the two paths diverge. The byte-level field maps stay active under the comparison, so you can keep your byte labels visible while you reason about the round trips.

Common HTTP/3 handshake bugs, and what they look like

Four HTTP/3 handshake bugs that look like a working handshake

The HTTP/3 QUIC Handshake Anatomy tool is designed to make these bugs visible, but it helps to know what to look for. Stuck on 1-RTT after Retry – the client sends Initial, server replies with Retry, but the client keeps the original DCID instead of adopting the SCID from the Retry packet. The server then times out because the new Initial’s DCID does not match. 0-RTT rejection with no early-data response – the server did not advertise the early_data extension in its EncryptedExtensions, and the client still sent early data. The server discards the early data and the client has to retransmit under 1-RTT keys. Connection migration re-handshakes – the client assumes the connection ID survives the network change but the server issued a new connection ID via NEW_CONNECTION_ID, and the client did not track it. The first packet on the new network is treated as a fresh connection.

The visualizer’s 0-RTT scenario is the one to use when you suspect the third class of bug. Toggle the Retry packet map on and you can see whether the server is sending a Retry at all; without that, you are debugging in the dark.

Where to go from here

The honest summary is that HTTP/3 is not a faster HTTP/2. It is a different transport that happens to carry HTTP semantics, and the handshake is the part of the protocol where that difference is most visible. Read RFC 9000 for the wire format, RFC 9114 for the HTTP/3 layer on top, and let the HTTP/3 QUIC Handshake Anatomy tool at Elysia Tools turn the byte streams into sequence diagrams you can label. Once you can read the long-header byte map from memory, the rest of the protocol is just streams and frames. Together with the TLS Handshake Anatomy and TCP Anatomy tools, the Network category at elysiatools.com/en/tools/network gives you enough to debug any handshake that crosses the public internet.

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *