TL;DR — Zero round‑trip (0‑RTT) resumption in TLS 1.3 lets a client resume a session without waiting for a full handshake, cutting latency by 30‑70 % in most microservice calls. The gain comes at the price of replay‑ability and reduced forward secrecy, so production teams must isolate 0‑RTT traffic, enforce strict replay detection, and choose between stateless tickets or server‑side session caches.
TLS 1.3 arrived with a promise: faster, safer connections. One of its most compelling features is Zero‑RTT (0‑RTT) resumption, where a client can send encrypted application data on the first flight of a resumed handshake. For latency‑sensitive workloads—API gateways, mobile back‑ends, or high‑frequency trading—those saved milliseconds translate into tangible revenue. This post walks through the end‑to‑end architecture, the security calculus, and performance patterns you can copy into NGINX, Envoy, or custom Go services.
Why Zero‑RTT Resumption Matters
- User‑perceived latency – A full TLS 1.3 handshake needs two round‑trips (client‑hello → server‑hello + encrypted extensions → finished). In a typical 40 ms WAN, that’s ~80 ms before any HTTP payload can travel. Zero‑RTT eliminates the second round‑trip entirely.
- Connection churn – Serverless functions, edge caches, and container‑per‑request architectures spin up new connections for each request. Re‑using a session ticket avoids the cryptographic cost of a fresh key exchange.
- Cost in CPU cycles – The initial handshake performs an X25519 (or P‑256) Diffie‑Hellman operation and a few hash calculations. Skipping it reduces CPU load on TLS‑terminating load balancers, freeing cycles for business logic.
Real‑world telemetry from Cloudflare shows a 30‑50 % reduction in tail latency for static‑asset delivery when 0‑RTT is enabled on repeat visitors. The benefit is most pronounced when the client is already authenticated and the server can safely assume the same identity.
TLS 1.3 Handshake Recap
Before diving into resumption, it helps to remember the baseline flow:
- ClientHello – includes supported groups, signature algorithms, and a key_share for the initial key exchange.
- ServerHello – selects a group, returns its own key_share, and optionally a session_ticket for future resumption.
- EncryptedExtensions, Certificate, CertificateVerify, Finished – each encrypted under the newly derived traffic secret.
- Application Data – now the channel is secure.
When a client possesses a valid session ticket, it can embed a pre_shared_key (PSK) extension in its next ClientHello, along with a early_data payload. The server, if it accepts the PSK, can immediately decrypt the early data and start processing without waiting for its own Finished message.
Architecture of Zero‑Round‑Trip Resumption
Session Ticket Issuance
TLS 1.3 decouples session state from the server by encrypting the ticket with a ticket encryption key (TEK). The ticket typically contains:
- Cipher suite identifier
- Selected key‑share group
- Resumption secret (PSK)
- Ticket lifetime (seconds)
- Optional application‑layer context (e.g., user ID, request routing hint)
Because the ticket is opaque to the server, stateless designs can simply store the TEK in a rotating key store (e.g., AWS KMS) and discard any per‑session memory.
# Example NGINX TLS ticket key rotation (nginx.conf)
ssl_ticket_key /etc/nginx/tls/ticket.key 4096; # 4096‑bit key, rotates daily via cron
Stateless vs. Stateful –
Stateless: Server only needs the TEK to decrypt tickets. No in‑memory session tables, ideal for horizontally scaled edge nodes.
Stateful: Server stores a mapping ticket_id → PSK in a fast cache (Redis, Memcached). This enables finer‑grained revocation (e.g., per‑user logout) but adds cache latency.
TLS 0‑RTT Flow Diagram
Client Server
| ClientHello (PSK + early_data) |
|------------------------------------>|
| ServerHello (accept PSK) |
| EncryptedExtensions |
| Finished |
|<------------------------------------|
| Application Data (already processed) |
The server may still send a NewSessionTicket in the same handshake, allowing the client to refresh its PSK for the next connection.
Security Trade‑offs
Zero‑RTT brings two major risks that must be mitigated at architectural level.
Replay Attacks
Early data is not bound to the server’s Finished message, so an adversary who intercepts a 0‑RTT packet can replay it verbatim until the ticket expires. Mitigations:
| Technique | How it works | Typical overhead |
|---|---|---|
| Replay cache | Store a hash of each early request (e.g., `SHA256(early_data | |
| Idempotent endpoints | Design APIs that safely handle duplicate submissions (e.g., use PUT with resource version). | No runtime cost, but requires careful API design. |
| Application‑layer nonce | Client includes a monotonic nonce signed with the PSK; server checks monotonicity. | Requires extra field in request body; negligible CPU. |
A concrete implementation in Go using Redis:
package main
import (
"crypto/sha256"
"encoding/hex"
"github.com/go-redis/redis/v8"
"context"
)
func isReplay(ctx context.Context, rdb *redis.Client, ticketID, payload []byte) (bool, error) {
h := sha256.Sum256(append(ticketID, payload...))
key := "tls0rtt:" + hex.EncodeToString(h[:])
set, err := rdb.SetNX(ctx, key, "1", 5*time.Second).Result()
return !set, err // false = not a replay, true = replay
}
Forward Secrecy Impact
In a full handshake, the server’s Finished message derives the application traffic secret from an ephemeral DH exchange, guaranteeing forward secrecy. With 0‑RTT, the early data is encrypted under a PSK that is derived from the original handshake’s DH secret but does not receive forward‑secrecy protection. If the server’s long‑term private key is compromised, an attacker who also captured the early data can decrypt it.
Mitigation strategies:
- Short ticket lifetimes – Limit the window where a compromised key can be useful (e.g., 5 minutes).
- Separate early data – Reserve 0‑RTT for non‑sensitive payloads (e.g., GET requests, cache lookups). Sensitive writes must fall back to a full handshake.
- Hybrid PSK – Combine the PSK with a fresh DH exchange for the resumed connection (TLS 1.3 already does this for post‑handshake traffic), preserving forward secrecy for later data.
Performance Patterns in Production
Latency Benchmarks
| Scenario | Avg RTT (ms) | TLS 1.2 Handshake (2‑RTT) | TLS 1.3 Full Handshake (2‑RTT) | TLS 1.3 0‑RTT Resumption |
|---|---|---|---|---|
| In‑region (AWS us‑east‑1) | 0.8 | 2.5 | 1.6 | 0.9 |
| Cross‑continent (US → EU) | 85 | 170 | 110 | 55 |
| Edge → Mobile 4G | 45 | 90 | 55 | 30 |
Numbers are from a synthetic benchmark using wrk2 with 100 k concurrent connections, measuring the time to first byte (TTFB). The 0‑RTT column includes the cost of replay‑cache lookups.
Integration with NGINX
NGINX 1.25+ supports TLS 1.3 0‑RTT via the ssl_early_data directive. A minimal config for an API gateway:
server {
listen 443 ssl http2;
ssl_certificate /etc/nginx/certs/api.pem;
ssl_certificate_key /etc/nginx/certs/api.key;
ssl_protocols TLSv1.3;
ssl_early_data on;
ssl_session_ticket_key /etc/nginx/tls/ticket.key;
# Optional replay cache using the ngx_http_redis_module
set $early_data_replay_key $ssl_session_id$ssl_early_data;
if ($redis2_reply = "EXISTS") {
return 425; # Too Early – client must retry without early data
}
redis2_pass 127.0.0.1:6379;
redis2_query setex $early_data_replay_key 5 1;
location / {
proxy_pass http://backend;
}
}
Key points:
ssl_early_data on;tells NGINX to accept early data.- The replay cache uses Redis
SETEXwith a 5‑second TTL. - If a replay is detected, NGINX returns 425 Too Early, prompting the client to retry with a full handshake.
Integration with Envoy
Envoy’s tls_context supports allow_early_data and can be paired with a filter that writes early‑data hashes to a local LRU cache.
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 443 }
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
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: backend }
http_filters:
- name: envoy.filters.http.router
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
tls_params:
tls_maximum_protocol_version: TLSv1_3
tls_certificate_sds_secret_configs:
- name: server_cert
sds_config:
path: /etc/envoy/secret.yaml
session_ticket_keys:
- filename: /etc/envoy/ticket.key
allow_early_data: true
To add replay protection, implement a custom HTTP filter (C++ or Lua) that extracts :early-data and stores a hash in an LRU cache. Envoy’s filter architecture ensures the check happens before the request reaches the upstream service.
Observability
When rolling out 0‑RTT, monitor:
- EarlyDataAccepted – Counter of successful 0‑RTT requests.
- EarlyDataRejected – Counter of replays or fallback handshakes.
- TicketExpiryRate – How many tickets expire before reuse (indicates ticket lifetime is too short or client patterns changed).
Prometheus snippets:
# HELP tls_0rtt_accepted Total successful 0‑RTT resumptions
# TYPE tls_0rtt_accepted counter
tls_0rtt_accepted{service="api-gateway"} 124563
# HELP tls_0rtt_rejected Total 0‑RTT attempts rejected (replay, policy)
# TYPE tls_0rtt_rejected counter
tls_0rtt_rejected{service="api-gateway"} 842
Key Takeaways
- Zero‑RTT resumption can cut TLS latency by 30‑70 %, especially across high‑latency networks.
- The trade‑off is replayability and reduced forward secrecy; mitigate with short ticket lifetimes, replay caches, and by restricting early data to idempotent or non‑sensitive operations.
- Stateless tickets simplify scaling of edge load balancers, while stateful caches give per‑user revocation at the cost of extra memory and latency.
- Production‑ready patterns include NGINX’s
ssl_early_datawith a Redis replay cache, and Envoy’sallow_early_datacombined with a custom filter. - Instrumentation (Prometheus counters, latency histograms) is essential to detect abuse and to tune ticket lifetimes.
Further Reading
- TLS 1.3 RFC 8446 – The definitive specification of TLS 1.3, including 0‑RTT semantics.
- Cloudflare Blog: “Zero‑RTT TLS 1.3 in Production” – Real‑world performance numbers and deployment lessons.
- Mozilla TLS Configuration Generator – Up‑to‑date recommendations for enabling TLS 1.3 and 0‑RTT safely