Loading content...
Every round trip matters. On a mobile network with 100ms latency, a traditional HTTPS connection requires 200-300ms just for TCP and TLS handshakes—before any application data flows. For a user clicking a link, this represents perceptible delay. For an application making many API calls, it accumulates into seconds of wasted time.
0-RTT (Zero Round Trip Time) is QUIC's answer to this latency tax. When a client reconnects to a server it has visited before, 0-RTT allows the client to send encrypted application data in its very first packet. The server can respond with data before the handshake completes. From the client's perspective, the connection is instantaneous.
This isn't magic—it's the culmination of careful cryptographic design inherited from TLS 1.3 and integrated deeply into QUIC's architecture.
By the end of this page, you will understand how 0-RTT builds on TLS 1.3 session resumption, the cryptographic mechanisms that enable sending encrypted data in the first packet, the security tradeoffs of 0-RTT (particularly replay attacks), how servers protect against 0-RTT replay, and when 0-RTT should and shouldn't be used.
Before diving into 0-RTT, let's quantify the latency problem QUIC solves.
Traditional TCP+TLS Connection:
A new HTTPS connection requires multiple round trips:
Total: 3 RTT before first byte of response
For TLS 1.2, the handshake is 2 RTT, making the total 4 RTT.
| RTT Latency | TCP+TLS 1.3 (3 RTT) | TCP+TLS 1.2 (4 RTT) | QUIC 1-RTT | QUIC 0-RTT |
|---|---|---|---|---|
| 20ms (datacenter) | 60ms | 80ms | 20ms | 0ms* |
| 50ms (good mobile) | 150ms | 200ms | 50ms | 0ms* |
| 100ms (typical mobile) | 300ms | 400ms | 100ms | 0ms* |
| 300ms (satellite/remote) | 900ms | 1200ms | 300ms | 0ms* |
*0-RTT latency is "0ms" in the sense that no round trips are required before application data is sent. The actual latency is approximately the one-way propagation time; the response arrives after 1 RTT from the client's perspective, but the request-response cycle begins immediately.
QUIC's Improvement:
New connection (1-RTT): QUIC combines transport and crypto handshake into a single round trip. Client sends ClientHello with transport parameters; server responds with complete handshake and can include response data. 1 RTT to first response data.
Resumption (0-RTT): Client sends ClientHello AND application data encrypted with previously established keys. Server can process the request immediately and send response before handshake completes. 0 RTT to first request transmission; 1 RTT to first response.
Think of 0-RTT not as eliminating all latency, but as eliminating added latency. With 0-RTT, reconnecting to a server costs no more than sending a single UDP packet. The only latency is the inherent network propagation time—the absolute physical minimum.
QUIC's 0-RTT builds directly on TLS 1.3's session resumption mechanism. Understanding TLS 1.3 resumption is essential for understanding QUIC 0-RTT.
Pre-Shared Keys (PSK):
After a successful TLS 1.3 handshake, the server can send the client a NewSessionTicket message containing a Pre-Shared Key (PSK). This PSK is derived from the connection's master secret and can be used to resume the session later.
The PSK is essentially a "remember me" token:
12345678910111213141516171819202122232425262728293031323334353637
TLS 1.3 Session Ticket Flow:============================ Initial Connection:-------------------Client Server | | |------- ClientHello ------------->| |<------ ServerHello --------------| |<------ EncryptedExtensions ------| |<------ Certificate ---------------| |<------ CertificateVerify ---------| |<------ Finished ------------------| |------- Finished ----------------->| | | |<=== Application Data Flows =====>| | | |<------ NewSessionTicket ----------| (Server issues PSK ticket) |<------ NewSessionTicket ----------| (May issue multiple) | | Ticket contents (encrypted by server):{ "session_id": "uuid...", "creation_time": 1705312800, "expiry_time": 1705399200, # Valid for 24 hours (configurable) "resumption_master_secret": "...", # Key derivation material "sni": "example.com", # Server Name Indication "alpn": "h3", # Application Protocol "max_early_data_size": 16384 # How much 0-RTT data allowed} Client stores:- Ticket blob (opaque)- Associated server identity- Max early data size- Creation timestampThe session ticket is encrypted with a key only the server knows. The client cannot read or modify it—it's opaque. This means the server controls what state it stores and how long tickets remain valid. Servers can issue tickets that are self-contained (all state encrypted in ticket) or that reference server-side state (ticket contains only a lookup key).
QUIC integrates TLS 1.3's 0-RTT into its packet structure, allowing early data to be sent in the very first UDP datagram. Let's trace through the complete 0-RTT handshake.
Client's First Packet:
When a client with a valid session ticket connects:
Initial Packet (Long Header, Initial encryption)
early_data extension0-RTT Packet (Long Header, 0-RTT encryption)
These packets are coalesced into a single UDP datagram, sending both handshake initiation and application data atomically.
12345678910111213141516171819202122232425262728293031323334
QUIC 0-RTT Connection: First UDP Datagram========================================== UDP Datagram (coalesced packets):┌─────────────────────────────────────────────────────────────┐│ Initial Packet ││ Long Header: Type=Initial, Version, DCID, SCID, Token ││ Payload (Initial Keys): ││ CRYPTO Frame: ClientHello ││ - Supported versions ││ - Cipher suites ││ - PSK identity (session ticket) ││ - early_data extension (requesting 0-RTT) ││ - Key share (for 1-RTT keys) ││ PADDING Frame: (to reach 1200 byte minimum) │├─────────────────────────────────────────────────────────────┤│ 0-RTT Packet ││ Long Header: Type=0-RTT, Version, DCID, SCID ││ Payload (0-RTT Keys, derived from PSK): ││ STREAM Frame: Stream 0, HTTP/3 request ││ "GET /api/user/profile HTTP/3..." ││ STREAM Frame: Stream 4, another request ││ "GET /api/notifications HTTP/3..." │└─────────────────────────────────────────────────────────────┘ 0-RTT Key Derivation: PSK → HKDF-Extract → Early Secret Early Secret → Derive-Secret → client_early_traffic_secret client_early_traffic_secret → HKDF-Expand → 0-RTT Key + IV Note: 0-RTT data is encrypted, but the encryption comes fromthe PSK (known from previous connection), not from this handshake.This is what makes the "0 round trips" possible—and also whatcreates the replay vulnerability.Server's Response:
The server, upon receiving the coalesced datagram:
early_data extensionThe client receives the response in approximately 1 RTT, with the request having been processed at the server immediately upon receipt—no waiting for handshake completion.
Only the client can send 0-RTT data. The server cannot send 0-RTT data to the client because the server doesn't have a PSK for the client (the relationship isn't symmetric). Server responses use 1-RTT keys, which are established during the handshake. This asymmetry reflects the typical client-initiated request pattern.
0-RTT's latency advantage comes with a fundamental security limitation: replay attacks. Understanding this tradeoff is essential for safe 0-RTT usage.
The Replay Vulnerability:
In a full handshake, both parties contribute fresh randomness that's mixed into the key derivation. An attacker can't replay an old connection—the new random values would produce different keys, and the replayed data wouldn't decrypt.
0-RTT data, however, is encrypted using keys derived entirely from the PSK (which doesn't change between connections). An attacker who captures a 0-RTT packet can replay it later:
123456789101112131415161718192021222324252627282930
0-RTT Replay Attack Example:============================= Original client request:Client → Server: Initial + 0-RTT Packet 0-RTT Data: POST /api/transfer {"from": "alice", "to": "bob", "amount": 100} Server processes: Transfers $100 from Alice to BobServer → Client: 200 OK, transfer complete Attacker captures the encrypted packet... Later replay by attacker:Attacker → Server: [Exact copy of captured packet] 0-RTT Data: (same encrypted bytes) POST /api/transfer ... If server has no replay protection: Server processes: Transfers another $100 from Alice to Bob! Server → ???: 200 OK, transfer complete (Response goes to attacker's IP, but damage is done) Key insight:- Attacker doesn't need to decrypt the 0-RTT data- Attacker doesn't need the PSK- Attacker only needs to capture and resend the packet- The encrypted data is the same, so it decrypts the same wayThis is not a bug or oversight—it's a fundamental tradeoff. To send encrypted data in zero round trips, the encryption must use pre-existing keys (the PSK). Pre-existing keys can be used by replayers. There is no cryptographic trick that provides 0-RTT encryption without replay vulnerability. The solution must be at the application layer.
Why 1-RTT Data Isn't Replayable:
For comparison, 1-RTT data (the normal case after handshake) uses keys derived from:
Because each handshake includes fresh random values and a new key exchange, the 1-RTT keys are unique to each connection. Replaying a captured packet from a previous connection would fail—the keys don't match.
Since replay protection can't be achieved cryptographically for 0-RTT, it must be achieved through other means. TLS 1.3 and QUIC define several strategies, each with tradeoffs.
Strategy 1: Single-Use Tickets
Each session ticket is valid only once. The server tracks used tickets and rejects duplicates.
Strategy 2: Application-Layer Idempotency
The most robust approach: design APIs so that repeating a request has no additional effect. This pushes replay protection to where it can be done correctly—the application.
Idempotent operations (safe for 0-RTT):
GET /api/user/profile — Reading data multiple times is harmlessPUT /api/user/settings — Setting to same value is equivalent to setting onceDELETE /api/session/{id} — Deleting already-deleted is no-opNon-idempotent operations (avoid in 0-RTT):
POST /api/order — Creates new order each timePOST /api/transfer — Moves money each timePOST /api/message/send — Sends duplicate messageFor non-idempotent operations, applications can add idempotency keys:
POST /api/transfer
Idempotency-Key: 7f3d9a2e-...
{"from": "alice", "to": "bob", "amount": 100}
Server stores completed idempotency keys and returns cached response on replay.
QUIC has no visibility into application semantics. It cannot distinguish a GET from a POST, or a read from a write. The responsibility for 0-RTT safety lies with the application layer. Applications MUST NOT send non-idempotent operations as 0-RTT data unless they implement their own replay protection.
Servers can reject 0-RTT data, and clients must handle this gracefully. Rejection can occur for various reasons:
Reasons for 0-RTT Rejection:
| Rejection Reason | Server Behavior | Client Recovery |
|---|---|---|
| Ticket expired | Ignore 0-RTT, proceed with 1-RTT handshake | Receive new ticket, retry as 1-RTT |
| Ticket decryption failed | Ignore 0-RTT, proceed with full handshake | Fallback to full handshake |
| Replay detected | Ignore 0-RTT, may continue handshake | Retry without 0-RTT; investigate if intentional |
| Server config changed | Incompatible ALPN/params, reject 0-RTT | Receive new config, retry with 1-RTT |
| 0-RTT disabled | Server policy doesn't allow 0-RTT | Always use 1-RTT for this server |
| Anti-replay window expired | Too old for strike register | Retry with 1-RTT, get fresh ticket |
1234567891011121314151617181920212223242526272829303132333435363738394041424344
0-RTT Rejection Flow:===================== Client sends:┌─────────────────────────────────────┐│ Initial Packet ││ CRYPTO: ClientHello with PSK │├─────────────────────────────────────┤│ 0-RTT Packet ││ STREAM: GET /api/data │└─────────────────────────────────────┘ Server decides to reject 0-RTT (ticket expired): Server sends:┌─────────────────────────────────────┐│ Initial Packet ││ CRYPTO: ServerHello ││ - NO early_data extension │ ← 0-RTT rejected│ - Continue with 1-RTT handshake │├─────────────────────────────────────┤│ Handshake Packet ││ CRYPTO: Encrypted Extensions ││ CRYPTO: Finished │├─────────────────────────────────────┤│ (No 1-RTT response data yet) │└─────────────────────────────────────┘ Client detects rejection: - ServerHello lacks early_data extension - 0-RTT data was NOT processed - Client MUST retransmit request as 1-RTT Client retransmits:┌─────────────────────────────────────┐│ 1-RTT Packet ││ STREAM: GET /api/data │ ← Same request, now 1-RTT│ CRYPTO: Finished │└─────────────────────────────────────┘ Server processes 1-RTT request normally. Total cost: 1 RTT extra (same as if 0-RTT never attempted)Client has new valid ticket for future 0-RTT attempts.0-RTT rejection is a normal part of the protocol, not an error. Clients should always be prepared for rejection and fall back gracefully. The connection still succeeds—just with 1-RTT latency instead of 0-RTT. Applications shouldn't assume 0-RTT will succeed.
Implementing 0-RTT correctly requires careful attention to several protocol and security details.
Transport Parameters and 0-RTT:
QUIC transport parameters (like max_stream_data, initial_max_data) are negotiated during the handshake. For 0-RTT, the client uses transport parameters stored with the session ticket—the parameters from the previous connection.
This creates a compatibility requirement: if the server's parameters change, 0-RTT might be rejected or constrained. For example, if the server previously allowed 64KB initial window but now allows only 32KB, the client cannot send more than 32KB of 0-RTT data.
Key Parameters for 0-RTT:
123456789101112131415161718192021222324252627282930313233343536373839404142
Session Ticket Stored Parameters:================================== When client receives NewSessionTicket after initial connection: Ticket = Encrypt_ServerKey({ psk_identity: random_bytes(32), resumption_secret: derived_from_handshake, ticket_nonce: unique_per_ticket, # Stored transport parameters (0-RTT uses these) saved_transport_params: { initial_max_data: 65536, initial_max_stream_data_bidi_local: 32768, initial_max_stream_data_bidi_remote: 32768, initial_max_stream_data_uni: 32768, initial_max_streams_bidi: 100, initial_max_streams_uni: 100, max_idle_timeout: 30000, max_udp_payload_size: 1472, }, # Application layer requirements alpn: "h3", server_name: "example.com", # Validity creation_time: 1705312800, expiry_time: 1705399200, # 24 hours later max_early_data_size: 16384,}) Client stores this ticket along with:- Server address- Server certificate chain (for verification)- Creation timestamp (for age calculation) On 0-RTT attempt:- Client uses saved_transport_params for flow control limits- Client sends at most max_early_data_size bytes- If server's current params differ, server may reject 0-RTT- After handshake, new parameters from ServerHello applyServers must balance ticket lifetime against security and freshness. Longer lifetimes improve 0-RTT hit rate (users returning after days still have valid tickets) but increase replay window and may use stale parameters. Typical production lifetimes range from 1 hour to 7 days, depending on risk tolerance.
HTTP/3 extends 0-RTT to the application layer with specific guidance on safe usage. Not all HTTP methods are safe for 0-RTT, and HTTP/3 provides mechanisms for safe early data.
HTTP Method Safety for 0-RTT:
| Method | Idempotent | 0-RTT Safe | Notes |
|---|---|---|---|
| GET | Yes | ✅ Safe | Read-only, naturally idempotent |
| HEAD | Yes | ✅ Safe | Same as GET without body |
| OPTIONS | Yes | ✅ Safe | Discovery, no side effects |
| PUT | Yes | ⚠️ Careful | Idempotent by spec, but check impl |
| DELETE | Yes | ⚠️ Careful | Deleting twice should be safe |
| POST | No | ❌ Unsafe | Creates resources, not replayable by default |
| PATCH | No | ❌ Unsafe | Partial update may not be idempotent |
| CONNECT | N/A | ❌ Unsafe | Establishes tunnel, not early data |
HTTP/3 Early Data Handling:
HTTP/3 clients should:
Early-Data header to signal 0-RTT to applicationThe Early-Data Header:
When a request is sent as 0-RTT, intermediaries (proxies) may add:
GET /api/data HTTP/3
Early-Data: 1
This header tells the origin server that the request arrived as early data. The server can then make informed decisions:
123456789101112131415161718192021222324252627282930313233343536373839404142
HTTP/3 0-RTT Example:===================== Client with valid session ticket for api.example.com: First packet contains: Initial: ClientHello with PSK 0-RTT: STREAM 0: HTTP/3 control stream setup STREAM 4: HEADERS frame GET /api/user/profile Host: api.example.com Accept: application/json Server processes 0-RTT: - Validates PSK, accepts early data - Decrypts HEADERS frame - Sees safe GET request - Processes request while completing handshake Server response (1-RTT encrypted): STREAM 4: HEADERS frame HTTP/3 200 OK Content-Type: application/json STREAM 4: DATA frame {"name": "Alice", "email": "..."} Timeline: T=0: Client sends first packet T=10ms: Server receives, processes GET immediately T=15ms: Server sends response (includes handshake) T=25ms: Client receives data Without 0-RTT (1-RTT): T=0: Client sends ClientHello T=10ms: Server receives, sends ServerHello T=20ms: Client receives, sends Finished + GET T=30ms: Server processes GET, sends response T=40ms: Client receives data Savings: 15ms (1 RTT) in this example.On 100ms mobile RTT: 100ms savings.Major deployments use 0-RTT extensively. Google reports 0-RTT success rates of 70-85% for return visitors, with significant latency improvements for mobile users. Cloudflare and Fastly enable 0-RTT for HTTP/3 by default, with application-layer safety ensured by method filtering.
We've explored QUIC's 0-RTT connection establishment in depth. Let's consolidate the key concepts:
What's next:
With 0-RTT understood, we'll complete our QUIC exploration with HTTP/3 Foundation—examining how QUIC serves as the transport layer for the next generation of HTTP. We'll see how HTTP/3 leverages QUIC's streams, eliminates head-of-line blocking, and represents the culmination of lessons learned from HTTP/1.1 and HTTP/2.
You now understand how QUIC's 0-RTT enables immediate data transmission for returning connections, the security tradeoffs involved, and the strategies for safe 0-RTT usage. You've seen how this integrates with TLS 1.3 session resumption and how HTTP/3 provides application-layer guidance for safe early data. Next, we'll explore HTTP/3 and how it builds on QUIC's capabilities.