QUIC 0-RTT Connection Resumption: Eliminating Handshake Latency at the Edge
Connection setup latency is the silent tax on every web interaction. When a user navigates to a new page, their browser must establish a transport connection and negotiate encryption before a single byte of content arrives. On a traditional TCP+TLS 1.2 stack, this costs 3 round trips before data flows. TLS 1.3 reduced that to 2 round trips (1-RTT TLS on top of the TCP handshake). QUIC collapses transport and encryption into a single protocol, achieving 1-RTT for new connections. But the real breakthrough is QUIC 0-RTT: for returning visitors, application data travels in the very first packet, eliminating handshake latency entirely. For CDN edge servers handling millions of reconnecting users, 0-RTT transforms the connection experience. This article covers how 0-RTT works, its security trade-offs, and production deployment strategies for edge infrastructure.
The Connection Latency Problem
Every network connection begins with a handshake. TCP requires a 3-way handshake (SYN, SYN-ACK, ACK) consuming one round trip. TLS adds its own handshake on top: TLS 1.2 needs 2 additional round trips, TLS 1.3 needs 1. The total cost before any application data can flow:
- TCP + TLS 1.2: 3 round trips (1 TCP + 2 TLS)
- TCP + TLS 1.3: 2 round trips (1 TCP + 1 TLS)
- TCP + TLS 1.3 with 0-RTT early data: 1 round trip (TCP handshake still required)
- QUIC 1-RTT: 1 round trip (transport + TLS combined)
- QUIC 0-RTT: 0 round trips (data in first packet)
For a user 200ms from the nearest CDN edge (common for users in developing regions connecting to distant PoPs), the difference between 3-RTT and 0-RTT is 600 milliseconds. That is the difference between a page that feels instant and one that feels sluggish. At CDN scale, where edge servers handle connections from every corner of the globe, 0-RTT is not an optimization — it is a fundamental capability for delivering the experience users expect.
How QUIC 0-RTT Works
QUIC 0-RTT builds on TLS 1.3's session resumption mechanism. When a client completes a regular 1-RTT QUIC handshake, the server sends a NewSessionTicket message containing encrypted session state. The client stores this ticket along with the transport parameters from the connection. On the next connection to the same server, the client uses the session ticket to derive a pre-shared key (PSK), encrypts application data with this PSK, and includes both the ClientHello and the encrypted 0-RTT data in its first flight of packets.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
The server receives the initial packet, decrypts the 0-RTT data using the PSK derived from the session ticket, and can begin processing the request immediately — before the handshake completes. The handshake finishes in the background, upgrading the connection to full 1-RTT keys with forward secrecy. The 0-RTT data itself does not have forward secrecy (it is encrypted with keys derived from a previous session's state), but subsequent data on the connection does.
Session Ticket Architecture
The session ticket is the foundation of 0-RTT. It contains the cipher suite, the resumption secret, the server's certificate verification status, and optionally application-specific data like ALPN negotiation results. The ticket's lifetime determines how long clients can attempt 0-RTT resumption — longer lifetimes increase 0-RTT hit rates but expand the replay window.
# QUIC 0-RTT session ticket structure (conceptual)
class SessionTicket:
def __init__(self):
self.ticket_nonce = os.urandom(16)
self.cipher_suite = "TLS_AES_256_GCM_SHA384"
self.resumption_secret = derive_resumption_secret(
handshake_secret, "res master"
)
self.ticket_age_add = random.randint(0, 2**32)
self.max_early_data_size = 16384 # bytes
self.lifetime = 7200 # seconds (2 hours)
self.creation_time = time.time()
# CDN-specific extensions
self.server_name = "edge-sjc-01.cdn.example"
self.transport_params = {
"initial_max_data": 10485760,
"initial_max_streams_bidi": 128,
"max_idle_timeout": 30000,
}
def encrypt(self, ticket_key):
"""Server encrypts ticket with rotating key."""
plaintext = serialize(self.__dict__)
return aead_encrypt(ticket_key, self.ticket_nonce, plaintext)
def is_valid(self, now):
age = now - self.creation_time
return age < self.lifetime
CDN Ticket Key Distribution
For 0-RTT to work across a CDN's edge network, all edge servers in a PoP (or ideally all PoPs) must share ticket encryption keys. A client that receives a session ticket from edge server A and reconnects to edge server B can only resume with 0-RTT if server B can decrypt the ticket. CDN operators solve this with a centralized ticket key distribution system that rotates keys on a schedule (typically every few hours) and distributes them to all edge servers.
# Ticket key rotation with overlap window
class TicketKeyManager:
def __init__(self):
self.current_key = None
self.previous_keys = []
self.rotation_interval = 3600 # 1 hour
def rotate(self):
if self.current_key:
self.previous_keys.append(self.current_key)
self.current_key = TicketKey(
key_id=os.urandom(4),
key_material=os.urandom(32),
created_at=time.time()
)
# Keep previous keys for 2 rotation intervals
# so tickets encrypted with old keys still work
cutoff = time.time() - (2 * self.rotation_interval)
self.previous_keys = [
k for k in self.previous_keys
if k.created_at > cutoff
]
def decrypt_ticket(self, encrypted_ticket):
"""Try current key first, then previous keys."""
for key in [self.current_key] + self.previous_keys:
try:
return aead_decrypt(
key.key_material,
encrypted_ticket
)
except DecryptionError:
continue
raise TicketDecryptionFailed()
The Replay Attack Problem
0-RTT data has a fundamental security limitation: it can be replayed. Because the client sends 0-RTT data before the handshake establishes freshness (the handshake's random values prove both parties are live), an attacker who captures the initial packet can re-send it to the server. The server, seeing a valid session ticket and properly encrypted data, will process the request again.
For CDN workloads, the impact depends on the request type. A replayed GET request for a cached image is harmless — the server returns the same cached response. A replayed POST request that submits a form, triggers a payment, or modifies a database record is catastrophic. This is why the HTTP specification designates GET and HEAD as safe (idempotent) methods and why CDN security configurations must restrict 0-RTT to these methods.
Anti-Replay Strategies for CDN Edge
CDN operators implement several anti-replay mechanisms, each with different trade-offs between security, performance, and operational complexity.
Strike Register
A strike register is a time-windowed data structure that stores a hash of each 0-RTT ClientHello. When the edge server receives a 0-RTT packet, it checks the register: if the ClientHello hash exists, the 0-RTT data is rejected (the connection falls back to 1-RTT). If not, the hash is recorded and the 0-RTT data is accepted. The register only needs to cover the maximum ticket lifetime — entries older than the ticket window can be evicted.
The challenge at CDN scale is coordination. If edge servers within a PoP do not share their strike registers, an attacker can replay to a different server in the same PoP. Solutions include shared in-memory stores (Redis-based registers) or partitioning: using anycast routing to ensure the same client IP consistently reaches the same server within a PoP.
Single-Use Tickets
An alternative to tracking replays is making replay impossible by design. With single-use tickets, the server issues a new session ticket for every connection and marks each ticket as consumed after its first 0-RTT use. This requires a distributed ticket state store (the server must check that a ticket has not been used at any other edge server), which adds latency to 0-RTT acceptance — somewhat defeating the purpose.
Bounded 0-RTT Window
A pragmatic approach used by several CDN providers is to limit the 0-RTT acceptance window to a very short duration — typically 10-60 seconds. The session ticket lifetime can be hours (for 1-RTT resumption), but 0-RTT is only accepted if the ticket age is within the bounded window. This narrows the replay opportunity while maintaining high 0-RTT acceptance rates for active browsing sessions where connections are reestablished frequently.
# CDN edge 0-RTT acceptance logic
def accept_zero_rtt(client_hello, early_data):
ticket = decrypt_session_ticket(client_hello.psk_identity)
if not ticket:
return Reject("invalid ticket")
# Check ticket age
ticket_age = time.time() - ticket.creation_time
if ticket_age > MAX_0RTT_WINDOW: # e.g., 30 seconds
return Reject("ticket too old for 0-RTT")
# Check strike register
ch_hash = hash(client_hello.serialize())
if strike_register.contains(ch_hash):
return Reject("replay detected")
strike_register.add(ch_hash, ttl=MAX_0RTT_WINDOW)
# Restrict to safe methods
request = parse_early_data(early_data)
if request.method not in ("GET", "HEAD"):
return Reject("unsafe method in 0-RTT")
# Check if request targets cacheable resource
if not is_cacheable_route(request.path):
return Reject("non-cacheable route")
return Accept(request)
Production Deployment Patterns
Deploying QUIC 0-RTT on CDN infrastructure requires coordination between the QUIC stack, the HTTP layer, and the origin shield. The implementation varies by QUIC library, but the general pattern is consistent.
Edge Server Configuration
# nginx-quic configuration for 0-RTT
server {
listen 443 quic reuseport;
listen 443 ssl;
ssl_protocols TLSv1.3;
ssl_early_data on;
# Session ticket configuration
ssl_session_tickets on;
ssl_session_timeout 2h;
ssl_session_ticket_key /etc/nginx/ticket_keys/current.key;
ssl_session_ticket_key /etc/nginx/ticket_keys/previous.key;
# QUIC-specific settings
quic_retry on; # Enable address validation for new connections
# Restrict early data to safe handlers
location /api/ {
# Reject 0-RTT for API endpoints
if ($ssl_early_data) {
return 425; # Too Early
}
proxy_pass http://origin;
}
location /static/ {
# Accept 0-RTT for static assets
proxy_set_header Early-Data $ssl_early_data;
root /var/www/cdn;
}
}
# Ticket key rotation cron (hourly)
# 0 * * * * /usr/local/bin/rotate-ticket-keys.sh
Origin Protection
Even when the CDN edge accepts 0-RTT data, the origin server should be aware that the request may be replayed. The standard mechanism is the Early-Data: 1 HTTP header, which the edge server sets on requests derived from 0-RTT data. The origin can use this header to apply additional caution — serving cached responses but not executing mutations, for example.
# Origin handler: respect Early-Data header
def handle_request(request):
is_early = request.headers.get("Early-Data") == "1"
if is_early:
# Only serve from cache, never execute side effects
cached = cache.get(request.path)
if cached:
return cached
# Return 425 Too Early to force 1-RTT retry
return Response(status=425, headers={
"Retry-After": "0"
})
# Full request processing for 1-RTT connections
return process_normally(request)
Measuring 0-RTT Effectiveness
Track these metrics to evaluate your 0-RTT deployment. The 0-RTT offer rate measures what percentage of connections attempt 0-RTT (clients with valid session tickets). The 0-RTT acceptance rate measures how many of those offers succeed. The 0-RTT rejection breakdown categorizes rejections by cause: expired ticket, strike register hit, unsafe method, non-cacheable route. A healthy deployment shows 60-80% offer rate (returning visitors) with 90%+ acceptance rate among offers.
# 0-RTT metrics collection
class ZeroRTTMetrics:
def record(self, event_type, details):
labels = {
"pop": self.pop_id,
"event": event_type,
"reason": details.get("reason", ""),
}
self.counter.increment(labels)
# Example log analysis
# 0-RTT offer rate: 72%
# 0-RTT acceptance rate: 94%
# Rejection breakdown:
# ticket_expired: 3.2%
# unsafe_method: 1.8%
# replay_detected: 0.01%
# non_cacheable: 0.99%
For CDN operators evaluating the performance impact, the key comparison is time-to-first-byte (TTFB) for 0-RTT connections versus 1-RTT connections, segmented by client distance to the edge. The improvement is most dramatic for high-RTT clients: a 200ms RTT client saves a full 200ms with 0-RTT, while a 10ms RTT client saves only 10ms. Prioritize 0-RTT optimization for edge PoPs serving regions with high last-mile latency.
QUIC 0-RTT represents the theoretical minimum for connection establishment — zero round trips before application data flows. Combined with HTTP/3 multiplexing and intelligent edge caching, it delivers the kind of instant-loading experience that separates leading CDN platforms from the rest. The security trade-offs are real but manageable with proper anti-replay mechanisms and method restrictions. For CDN operators, the question is not whether to deploy 0-RTT, but how aggressively to expand its scope.