QUIC’s 0-RTT early data allows clients to send application data along with their initial connection request, effectively skipping the entire TLS handshake roundtrip.

Imagine you’re trying to have a conversation with someone across a large room. Normally, you’d have to shout "Hey!" and wait for them to acknowledge, then you’d say "Can you hear me?" and wait for a reply, and only then could you start your actual message. QUIC’s 0-RTT is like being able to shout your entire message – "Hey, can you hear me? I want to tell you about the weather!" – all in one go, without waiting for any back-and-forth. This is incredibly powerful for reducing latency, especially for users on high-latency networks or for frequent, small requests.

Here’s what it looks like in practice. Let’s say a client wants to fetch a small resource from a server.

Client (Initiating 0-RTT):

GET /resource.txt HTTP/3
Host: example.com
Early-Data: 1

The client, having previously connected to example.com and established a secret (a "pre-shared key" or PSK), can now encrypt GET /resource.txt using that PSK. This encrypted data is sent as part of the initial Initial packet (the very first packet sent in a QUIC connection).

Server (Receiving 0-RTT):

The server receives this Initial packet. If it recognizes the client’s identifier and can successfully decrypt the early data using the stored PSK, it can immediately process the request.

HTTP/3 200 OK
Content-Length: 10
...
Hello World

The server then sends back its response, potentially as part of the handshake packets it would normally send anyway. The key is that the application data was sent before the full cryptographic handshake completed.

So, how does this magic happen? It all hinges on the TLS 1.3 handshake, which QUIC embeds within its own transport handshake.

  1. First Flight (Client -> Server): The client sends an Initial QUIC packet containing a TLS ClientHello. If the client has previously established a full TLS session with the server and received a NewSessionTicket (which contains the PSK and its associated metadata), it can also include encrypted application data within this Initial packet. This data is encrypted using a key derived from the PSK.
  2. Second Flight (Server -> Client): The server receives the Initial packet. It attempts to decrypt the early data. If successful, it processes the application data (e.g., fetches /resource.txt). The server then sends back its TLS ServerHello and EncryptedExtensions, and crucially, if it accepted the early data, it will send a Finished message. If it didn’t accept the early data (e.g., the PSK was invalid or expired), it will simply proceed with a standard handshake without early data.
  3. Third Flight (Client -> Server): The client receives the server’s Finished message. If the handshake completes successfully, the client knows its early data was accepted. If the server rejected the early data, the client would then resend the application data in a new, unencrypted (or rather, handshake-encrypted) packet.

The core problem this solves is the "connection setup latency." For a typical TLS 1.3 handshake, you need at least one roundtrip (ClientHello, ServerHello, Certificate, Finished). QUIC, by integrating TLS, can do this handshake in one roundtrip in most cases. 0-RTT further optimizes this by allowing application data to piggyback on the first packet, eliminating that initial roundtrip entirely for the application data itself.

The "secret" that enables 0-RTT is a Pre-Shared Key (PSK) established during a previous full TLS handshake. When the server successfully completes a TLS handshake with a client, it can issue a NewSessionTicket that contains a PSK. The client stores this ticket. On subsequent connection attempts, the client can use this PSK to derive the necessary encryption keys to send 0-RTT data. This PSK is not the same as the session keys established during the handshake; it’s a key specifically for bootstrapping the next session’s early data.

The critical lever you control here is the server’s configuration for issuing and accepting NewSessionTickets and the client’s ability to store and present them. On the server side, this is often tied to TLS configuration. For example, in OpenSSL, you’d configure session_ticket callbacks. In web servers like Nginx, you might configure ssl_session_tickets on; and ssl_session_cache shared:SSL:10m;. The client’s behavior is usually handled by the HTTP/3 library it’s using.

The most surprising thing about 0-RTT is that the data sent is not inherently replay-protected by QUIC itself. While the TLS 1.3 handshake encrypts the data, the PSK used for 0-RTT is the same for multiple connections. This means that if an attacker intercepts an encrypted 0-RTT packet and the server processes it, the attacker could theoretically replay that same packet later. The server must implement its own application-level replay protection if this is a concern. This is why 0-RTT is typically recommended only for idempotent operations (like GET requests) or for data where replay is either impossible or harmless.

Once you’ve got 0-RTT working, the next thing you’ll likely grapple with is connection migration.

Want structured learning?

Take the full Quic course →