TL;DR — TLS 1.3 zero round‑trip (0‑RTT) resumption can shave 30‑50 ms off latency for repeat connections, but it introduces replay risk and requires careful ticket management. By centralizing session ticket stores, tightening replay detection, and tuning NGINX/Envoy configs, you can reap performance gains while keeping the attack surface manageable.
Modern services that serve billions of requests per day cannot afford even a few milliseconds of extra latency. TLS 1.3’s 0‑RTT resumption promises “instant” handshakes for returning clients, but the shortcut comes with nuanced security trade‑offs and operational challenges. This post walks through the end‑to‑end architecture of a production‑grade 0‑RTT implementation, examines the security implications, and provides concrete performance‑tuning recipes for the most common edge proxies (NGINX, Envoy) and application runtimes (Go, Java).
Why Zero Round‑Trip Resumption Matters
- Latency budget – Mobile browsers on 4G/5G networks typically see 30 ms round‑trip times. Adding a full TLS handshake (≈2 RTTs) can consume 60–80 ms, which directly impacts page‑load metrics like First Contentful Paint.
- Connection churn – Microservice‑to‑microservice traffic often opens short‑lived TCP connections (e.g., HTTP/1.1 keep‑alive disabled). Each new TLS handshake becomes a hidden cost.
- Cost of CPU – TLS handshakes are CPU‑intensive (ECDHE key exchange, certificate validation). Reducing handshakes by 30 % can lower TLS‑related CPU usage by 15–20 % on edge nodes.
Zero round‑trip resumption (0‑RTT) allows a client to send encrypted application data immediately after the ClientHello, using a previously issued session ticket. The server can either accept the data instantly (fast path) or reject it and fall back to a full handshake (slow path). The fast path eliminates the extra RTT, delivering the latency benefit.
TLS 1.3 Handshake Recap
Before diving into 0‑RTT, it helps to recall the baseline TLS 1.3 flow:
ClientHello --> ServerHello, EncryptedExtensions, Certificate, CertificateVerify, Finished
<-- ServerFinished
One round‑trip (ClientHello → ServerHello) plus a post‑handshake Finished exchange. In 0‑RTT, the client reuses a session ticket received from a prior handshake:
ClientHello (with pre_shared_key) --> ServerHello, EncryptedExtensions, Finished
<-- ServerFinished
If the server validates the ticket, it can start processing the client’s early data right away.
Architecture for 0‑RTT Resumption
Designing a robust 0‑RTT pipeline involves three moving parts:
- Ticket Issuance – The server encrypts a session ticket with a secret key and sends it to the client.
- Ticket Store – A shared, high‑availability store for the ticket decryption keys (also called PSK secret). All edge nodes must be able to decrypt tickets issued anywhere in the fleet.
- Replay Mitigation – A lightweight mechanism to detect and reject replayed early data.
Session Ticket Lifecycle
| Phase | Actor | Action |
|---|---|---|
| Issuance | TLS library (e.g., OpenSSL, BoringSSL) | Generates a random ticket, encrypts it with the ticket‑encryption key (TEK), and sends it in a NewSessionTicket TLS message. |
| Key Rotation | Key Management Service (KMS) | Rotates TEK every 12–24 h, stores old keys for at least the ticket lifetime (default 7 days). |
| Decryption | Any edge node handling a client’s 0‑RTT attempt | Looks up the appropriate TEK from the shared store, decrypts the ticket, derives the pre‑shared key (PSK). |
Implementation tip: Store TEKs in a distributed KV store like Consul, Etcd, or AWS Parameter Store with versioned keys (tls/tek/2026-05-25). Edge nodes poll for changes every minute.
Integration with Load Balancers
Most production environments terminate TLS at the edge (NGINX, Envoy, HAProxy). Enabling 0‑RTT requires two configuration steps:
- Expose ticket keys – Provide the TLS library with the current TEK list. In NGINX, this is done via the
ssl_session_ticket_keydirective. - Enable early data – Turn on the
ssl_early_dataflag and set a reasonablessl_early_data_timeout.
NGINX Example
# nginx.conf snippet
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
# Load a rotating set of ticket keys (binary files generated by openssl)
ssl_session_ticket_key /etc/nginx/tls/ticket_key_2026-05-25.key;
ssl_session_ticket_key /etc/nginx/tls/ticket_key_2026-05-24.key; # fallback for old tickets
# Enable 0‑RTT
ssl_early_data on;
ssl_early_data_timeout 5s; # reject early data after 5 seconds if not accepted
Envoy Example
# envoy.yaml snippet
static_resources:
listeners:
- name: listener_0
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http_protocol_options:
allow_early_data: true
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_params:
tls_maximum_protocol_version: TLSv1_3
tls_certificate_sds_secret_configs:
- name: server_cert
sds_config:
path: /etc/envoy/tls/certs.yaml
session_ticket_keys:
- filename: /etc/envoy/tls/ticket_key_2026-05-25.key
- filename: /etc/envoy/tls/ticket_key_2026-05-24.key
Both proxies respect the ticket key rotation automatically when you reload the configuration (e.g., nginx -s reload).
Patterns in Production
| Pattern | When to Use | Benefits |
|---|---|---|
| Central Ticket Store | Multi‑region fleets with independent edge nodes | Guarantees any node can decrypt tickets, eliminates “ticket‑not‑found” errors during failover. |
| Stateless Replay Cache | High‑traffic APIs where early data is idempotent (e.g., GET, POST with idempotency key) | Minimal memory, O(1) lookup using a Bloom filter or Redis SET with TTL = ticket lifetime. |
| Hybrid Mode | Services that cannot tolerate any replay (e.g., financial transactions) | Accept early data only for safe methods; fallback to full handshake for unsafe methods. |
Security Trade‑offs
0‑RTT is not a free lunch. The primary concern is replay attacks: an attacker who captures a 0‑RTT packet can resend it to the server within the ticket’s validity window, potentially causing duplicate actions.
Replay Attacks
- Scope – Any data that changes state (POST, PUT, DELETE) is vulnerable.
- Mitigation – Use application‑level idempotency keys, or restrict 0‑RTT to read‑only methods.
Note – Cloudflare’s “0‑RTT for GET only” policy is a pragmatic compromise that preserves latency gains while eliminating most replay risk.
Forward Secrecy Considerations
TLS 1.3 still provides forward secrecy (FS) for the handshake keys, but the early data is encrypted with a PSK derived from the ticket. If an attacker later obtains the ticket‑encryption key, they can decrypt all early data encrypted with that ticket. Therefore:
- Key Rotation Frequency – Rotate TEKs at least daily. Shorter lifetimes reduce the window of exposure.
- Hardware‑Backed Secrets – Store TEKs in an HSM or AWS KMS with automatic rotation, limiting plaintext exposure.
Downgrade & Compatibility
Not all clients support 0‑RTT (e.g., older browsers, some Java versions). The server must gracefully fall back:
# Detect early data support via OpenSSL s_client
openssl s_client -connect example.com:443 -tls1_3 -early_data 1
If early_data is not accepted, the server proceeds with a normal handshake. This fallback is automatic in most TLS libraries; you only need to ensure the configuration does not force early data.
Performance Optimization in Production
Measuring the Impact
- Baseline – Capture latency with a full TLS handshake using
wrkorhey. - Enable 0‑RTT – Deploy the config changes, warm up the ticket store, then repeat the measurement.
- Compare – Expect a 30–50 ms reduction per connection on typical WAN RTTs.
Example Benchmark Script (bash)
#!/usr/bin/env bash
URL="https://api.example.com/health"
DURATION=30
echo "=== Baseline (full handshake) ==="
wrk -t12 -c400 -d${DURATION}s $URL > baseline.txt
echo "=== With 0‑RTT ==="
# Assume Envoy is now configured for early data
wrk -t12 -c400 -d${DURATION}s $URL > zerortt.txt
# Simple diff of average latency
grep "Latency" baseline.txt
grep "Latency" zerortt.txt
Tuning NGINX for High Throughput
- Increase
ssl_session_cachesize – Store more session tickets in memory to avoid KV lookups on every request. - Enable
ssl_session_tickets– Ensure tickets are issued even for HTTP/2 streams. - Adjust
worker_processes– Match the number of CPU cores; TLS decryption is CPU‑bound.
ssl_session_cache shared:SSL:10m; # ~400k tickets
ssl_session_timeout 1d;
worker_processes auto;
Envoy Optimizations
early_data_max_size– Limit the size of early data to prevent abuse (default 16 KB). Set to the maximum payload your API accepts for GET requests.early_data_cache– Use an in‑memory LRU cache for replay detection; configure TTL to match ticket lifetime.
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
early_data_config:
max_early_data_size: 8192
replay_protection:
cache:
name: envoy.extensions.common.cache.simple
typed_config:
"@type": type.googleapis.com/envoy.extensions.common.cache.simple.v3.SimpleCacheConfig
max_entries: 1000000
ttl: 86400s # 24h
Application‑Side Adjustments (Go)
Go’s crypto/tls package exposes Config.ClientSessionCache for clients and Config.GetConfigForClient for servers. To enable 0‑RTT:
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS13,
// Enable early data for servers
Enable0RTT: true,
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
// Attach a replay cache (e.g., in‑memory map with mutex)
return &tls.Config{
// Your normal server config
}, nil
},
}
On the client side, set tls.Config.ClientSessionCache to a tls.NewLRUClientSessionCache(128) and use Conn.Write immediately after Handshake().
Monitoring and Alerting
- Replay rate – Increment a Prometheus counter each time early data is rejected due to replay detection.
- Ticket decryption failures – Alert if >0.1 % of 0‑RTT attempts fail to decrypt tickets (indicates key sync issues).
- CPU utilization – Compare TLS CPU usage before and after enabling 0‑RTT; a successful rollout should show a measurable drop.
# Prometheus rule example
- alert: TLSZeroRTTReplayDetected
expr: rate(tls_early_data_replay_total[5m]) > 0.01
for: 2m
labels:
severity: warning
annotations:
summary: "Replay attempts detected on {{ $labels.instance }}"
description: "More than 1 % of early data requests are being rejected due to replay detection."
Key Takeaways
- Latency win: 0‑RTT can cut TLS handshake latency by half on typical WAN round‑trips, translating to 30–50 ms per request.
- Centralized ticket keys: Store TEKs in a distributed KV store and rotate them daily to maintain forward secrecy.
- Replay mitigation: Use stateless caches, idempotency keys, or restrict 0‑RTT to read‑only methods to keep replay risk low.
- Edge configuration matters: Enable
ssl_early_data(NGINX) orallow_early_data(Envoy) and keep a rolling set of ticket key files. - Observability: Track replay rates, ticket decryption failures, and TLS CPU usage to verify that the deployment is both safe and performant.
Further Reading
- TLS 1.3 RFC 8446 (IETF) – The official specification, including the 0‑RTT section.
- Cloudflare Blog: “Understanding TLS 1.3 0‑RTT” – A clear explanation of replay risks and mitigation strategies.
- Mozilla TLS Configuration Generator – Best‑practice TLS settings for servers, including session ticket handling.
