0-RTT

Protocol

0-RTT (zero round-trip time), or early data, is a TLS 1.3 handshake mode: a client holding a pre-shared key from an earlier connection sends encrypted application data in the first flight, before the handshake completes. QUIC reuses it. Early data is replayable, so only safe methods belong in it.

Also known as Early Data, Zero Round-Trip Time Resumption, 0RTT.

14 min read Updated Aug 30, 2026

Full Explanation

0-RTT (zero round-trip time) is a handshake mode of TLS 1.3. TLS 1.3 calls it "early data". A client that already shares a pre-shared key (PSK) with the server sends encrypted application data in the first flight of a new connection. It does this before the handshake completes (RFC 8446, section 2.3). QUIC reuses the mechanism. That is how HTTP/3 gets it (RFC 9001, section 4.6). 0-RTT is not a protocol of its own. It is also not the same thing as session resumption: resuming with a PSK but without 0-RTT still costs one round trip before the client can send a request. It is not TCP Fast Open either. TCP Fast Open carries data in the SYN and saves the transport round trip rather than the TLS one (RFC 7413). In web delivery the PSK arrives as a session ticket from an earlier connection. So a first-time visitor cannot use 0-RTT. TLS 1.2 has no equivalent (RFC 8446, section 1.2). The saving is one round trip on the request. It comes with a real weakness: early data is not forward secret and can be replayed. So only content that is safe to replay belongs in it.

How it works

0-RTT needs a PSK that both ends already hold. TLS allows a PSK provisioned out of band. In CDN practice, though, it comes from a previous handshake. At any time after it has received the client's Finished message, the server MAY send a NewSessionTicket message. This message "creates a unique association between the ticket value and a secret PSK derived from the resumption master secret" (RFC 8446, section 4.6.1). Servers MUST NOT set a ticket lifetime greater than 604800 seconds (7 days). Clients MUST NOT cache tickets for longer than 7 days whatever the lifetime says. RFC 9001 restates that seven-day ceiling for QUIC (RFC 9001, section 4.6).

On the next connection the client offers the ticket in the "pre_shared_key" extension. It MUST also supply the "early_data" extension in its ClientHello, then sends application data immediately (RFC 8446, section 4.2.10). That data is encrypted solely under keys derived from the offered PSK (section 2.3). The max_early_data_size value in the ticket's "early_data" extension caps how much the client may send (section 4.6.1). QUIC is different here. It does not use TLS early data at all. So it repurposes max_early_data_size as the sentinel 0xffffffff, and bounds the volume of 0-RTT data with the server's initial_max_data transport parameter instead (RFC 9001, section 4.6.1).

A server that receives an "early_data" extension MUST behave in exactly one of three ways (RFC 8446, section 4.2.10):

  • Accept: return its own "early_data" extension in EncryptedExtensions and process the data. It cannot accept only a subset. To accept, it must have selected the first PSK the client offered. The TLS version, cipher suite and ALPN protocol must also match those associated with that PSK.
  • Ignore the extension and return a regular 1-RTT response. It skips the early data by discarding records that fail deprotection under the handshake traffic key, up to max_early_data_size.
  • Send a HelloRetryRequest. This likewise skips the early data. The client MUST NOT include "early_data" in its follow-up ClientHello.

When early data is accepted, the client signals the key change with an EndOfEarlyData message after the server's Finished. When it is rejected, the client MUST start sending again as though the connection were new. Any request sent in early data has to be sent again (RFC 8470, section 4). TLS implementations MUST NOT automatically resend rejected 0-RTT data unless the application instructs them to (RFC 8446, appendix E.5).

QUIC carries early data in dedicated 0-RTT packets that form part of the first flight (RFC 9000, section 17.2.3). A ticket advertises QUIC 0-RTT by carrying the early_data extension. Omitting it tells the client the server does not accept 0-RTT (RFC 9001, section 4.6.1). A QUIC server accepts by sending EncryptedExtensions with an early_data extension. It rejects by sending EncryptedExtensions without one. It always rejects 0-RTT if it sends a HelloRetryRequest (RFC 9001, section 4.6.2). On rejection, every connection characteristic the client assumed might be wrong: application protocol, transport parameters, application configuration. So the client MUST reset the state of all streams. A client MAY reattempt 0-RTT if it receives a Retry or Version Negotiation packet, because neither signifies rejection.

Why it matters for a CDN

Count the round trips that pass before a returning client's first request byte reaches the edge:

  • Over TCP the three-way handshake goes first: SYN, then SYN-ACK, then the client's ACK. The ACK may already carry data. That is one round trip (RFC 9293, section 3.5, Figure 6).
  • The TLS handshake then adds "the one or two round-trip delays needed for the TLS handshake" (RFC 8470, section 1).
  • Resuming with a PSK but without 0-RTT collapses that to one round trip. In RFC 8446's Figure 3, the client's application data follows the server's flight (RFC 8446, section 2.2).
  • With 0-RTT the request leaves in the first flight. So no handshake round trip passes before the client's data. Under QUIC there is no separate transport handshake to pay for either, because QUIC "relies on a combined cryptographic and transport handshake to minimize connection establishment latency" (RFC 9000, section 7). So an HTTP/3 0-RTT request can ride in the very first datagram.

Read the name carefully. 0-RTT describes the client's send delay, not the whole exchange. RFC 8446 explains that "the 0-RTT data is just added to the 1-RTT handshake in the first flight" (RFC 8446, section 2.3). So the response still costs roughly one round trip. The saving is one round trip per new connection, and it is absolute. So it is worth most where the path is slow. Cloudflare points at clients who "frequently visit your application or connect over mobile networks" (Cloudflare docs). Akamai frames the payoff as reduced time to first byte on TLS 1.3 connections (Akamai docs). For delivery that means the entire win lands on repeat connections terminated at the edge. It is worth nothing to a first-time visitor, and nothing on a connection that is already open. It also stops at the edge. Cloudflare states the resumption is "only established between the client and Cloudflare" and "does not extend to the origin server". Fastly supports 0-RTT only between Fastly and requesting clients, not towards origin.

What CDNs do

  • Cloudflare: 0-RTT Connection Resumption is not enabled by default. You turn it on under Speed > Settings > Protocol Optimization, or with a PATCH to the "0rtt" setting in the API. It is available on the Free, Pro, Business and Enterprise plans. Cloudflare supports 0-RTT for GET, HEAD and OPTIONS requests, and not for POST. It adds an "Early-Data: 1" header to 0-RTT requests so origins can recognise them (Cloudflare docs).
  • Fastly: "TLS 1.3 is now the default TLS version." TLS configurations come as "HTTP/3 & TLS v1.3" or "HTTP/3 & TLS v1.3 + 0RTT". Accounts that enabled TLS for the first time on or after 29 March 2022 get the + 0RTT configuration by default. By default Fastly answers only idempotent requests over 0-RTT: GET and HEAD without query parameters. Such requests carry "Early-Data:1" per RFC 8470, readable in VCL as req.http.early-data (Fastly docs).
  • Akamai: "Early Data (0-RTT)" is a Property Manager behavior you add and switch on. It supports both QUIC-based traffic and TCP, and it requires TLS 1.3 to be enabled on the certificate. Akamai supports it only for GET requests with no additional query string parameters. It lets you scope it by hostname or by percentage of clients, and forwards "Early-Data: 1" to origin so the origin can answer 425. It cannot coexist with the Enforce mTLS settings behavior. Akamai's own caveats list reduced forward security and replay attacks (Akamai docs).
  • CloudFront: the viewer-facing documentation covers TLS 1.3 security policies and HTTP/3. For HTTP/3, "viewers must support TLSv1.3 and Server Name Indication (SNI)". Neither page documents 0-RTT or early data (supported protocols and ciphers, distribution settings). Read that as not documented rather than not supported.
  • Rolling your own edge or origin on nginx: the directive is ssl_early_data, default off. The variable $ssl_early_data returns "1" if TLS 1.3 early data is used and the handshake is not complete. Forward it with "proxy_set_header Early-Data $ssl_early_data" so the application can decide. nginx disables OpenSSL's built-in replay protection "because it interferes with session resumption". You restore it with "ssl_conf_command Options AntiReplay". For HTTP/3, 0-RTT needs OpenSSL 3.5.1 or higher, or BoringSSL, LibreSSL or QuicTLS. Before nginx 1.29.1, 0-RTT could not be enabled with OpenSSL at all, whatever ssl_early_data said (ngx_http_v3_module).

Watch out for

Early data trades away two guarantees. It "is not forward secret, as it is encrypted solely under keys derived using the offered PSK". And because "0-RTT data does not depend on the ServerHello" there are no guarantees of non-replay between connections. So an attacker who captures a first flight can send it again (RFC 8446, section 2.3). Content in early data must therefore be engineered to be safe under replay. RFC 8446 says "minimally, this means idempotent, but in many cases may also require other stronger conditions, such as constant-time response" (RFC 8446, appendix E.5). The same appendix lists the attacks beyond a plain duplicate: reordering replayed messages (moving a delete after a create), and cache-timing probes. A cache-timing probe replays a 0-RTT message to a different cache node, then measures request latency on a separate connection to see whether both requests hit the same resource. That last one is aimed squarely at a CDN.

In HTTP the deciding property is what a replayed copy would do:

  • Safe methods request no state change. Of the methods RFC 9110 defines, "the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe" (RFC 9110, section 9.2.1).
  • Idempotent methods give the same intended effect when repeated. "PUT, DELETE, and safe request methods are idempotent" (RFC 9110, section 9.2.2). GET is both safe and idempotent. POST is neither.
  • A replayed GET is usually harmless. A replayed POST duplicates "actions which cause side effects (e.g., purchasing an item or transferring money)" (RFC 8446, appendix E.5).
  • Absent other information, clients MAY send safe methods in early data. They MUST NOT send unsafe methods, or methods whose safety is not known (RFC 8470, section 4).

Anti-replay at the TLS layer is the server's job. RFC 8446 offers three mechanisms: single-use tickets (section 8.1), recording ClientHello values inside a bounded time window (section 8.2), and freshness checks on the obfuscated ticket age (section 8.3). RFC 8446 states that "servers SHOULD provide that level of replay safety by implementing one of the methods described in this section or by equivalent means" (RFC 8446, section 8). Across an edge fleet this is the hard part. RFC 8446 names the failure directly. If a server system "has multiple zones where tickets from zone A will not be accepted in zone B", an attacker can duplicate a ClientHello and its early data to both zones. Zone A accepts it as 0-RTT. Zone B forces a full handshake. If the attacker blocks zone A's ServerHello, the client completes the handshake with zone B and probably retries the request. That duplicates the request on the system as a whole. The floor is per-instance. A server MUST ensure that any instance of it "would accept 0-RTT for the same 0-RTT handshake at most once". That caps replays at the number of server instances. In a CDN with many points of presence, that number is large. Single-use tickets are the strictest option and the most expensive one. That is because they require "sharing the session database between server nodes in environments with multiple distributed servers, it may be hard to achieve high rates of successful PSK 0-RTT connections when compared to self-encrypted tickets" (RFC 8446, section 8.1).

Once an intermediary is in the path, the hazard moves to the origin. An intermediary that forwards a request before its TLS handshake with the client completes MUST send it with the "Early-Data" header field set to "1". A server "cannot make a request that contains the Early-Data header field safe for processing by waiting for the handshake to complete", because the request was early on a previous hop. Requests carrying that header that cannot be safely processed MUST be rejected with 425 (Too Early) (RFC 8470, section 5.1). User agents that sent a request in early data are expected to retry it on a 425. But "any retries MUST NOT be sent in early data" (RFC 8470, section 5.2). This matters most for anyone putting a CDN in front of an application: a gateway MUST NOT forward requests received in early data "unless it knows that the origin server it will forward to understands the Early-Data header field and will correctly generate a 425 (Too Early) status code". Where no origin supports it, "it is more efficient to disable early data entirely" (RFC 8470, section 6.1).

Under QUIC the restriction is stronger, and it is often overlooked. RFC 9001 states that "an application protocol that uses QUIC MUST include a profile that defines acceptable use of 0-RTT; otherwise, 0-RTT can only be used to carry QUIC frames that do not carry application data" (RFC 9001, section 5.6). HTTP/3 satisfies that requirement by adopting RFC 8470: "the anti-replay mitigations in [HTTP-REPLAY] MUST be applied when using HTTP/3 with 0-RTT" (RFC 9114, section 10.9). HTTP/3 also adds an acceptance condition of its own. This is a common cause of 0-RTT rejection after an edge configuration change: a client attempting 0-RTT MUST comply with the HTTP/3 settings stored from the previous session. And "if the server cannot determine that the settings remembered by a client are compatible with its current settings, it MUST NOT accept 0-RTT data" (RFC 9114, section 7.2.4.2).

Best practice

  • Enable 0-RTT only where the origin recognises "Early-Data: 1" and answers 425 (Too Early) for anything it cannot process safely. If nothing behind the edge does, leave it off.
  • Keep early data to safe methods. GET and HEAD are the practical set. The major CDNs enforce roughly that: Cloudflare allows GET, HEAD and OPTIONS. Fastly and Akamai narrow it further, to GET/HEAD or GET without query parameters.
  • Have the application reject non-idempotent requests that arrive with "Early-Data: 1" using 425 rather than processing them. This way a replay cannot double-apply a write. Make every instance behave the same way: inconsistent instances are what turns a replay into a duplicated request.
  • Decide the anti-replay state deliberately. Single-use tickets need a session database shared across nodes. ClientHello recording needs a bounded window plus freshness checks. Whichever you pick, a given 0-RTT handshake must be accepted at most once per instance. The replay ceiling is the instance count.
  • Do not put anything in early data whose timing leaks something. The cache-timing probe in RFC 8446 appendix E.5 works by replaying a request to a second cache node. So keep cacheable-but-sensitive lookups out of 0-RTT.
  • Measure it as one round trip saved per new connection from a returning client, not as a general latency fix. It does nothing for first-time visitors, nothing for connections already open, and nothing between edge and origin.

Examples

# TLS 1.3 0-RTT in Nginx
server {
    listen 443 ssl;
    ssl_protocols TLSv1.3;
    ssl_early_data on;  # Enable 0-RTT
    
    # Protect against replay attacks
    proxy_set_header Early-Data $ssl_early_data;
    # Backend can check this header and reject
    # non-idempotent requests with Early-Data: 1
}

# QUIC 0-RTT (automatic with HTTP/3)
server {
    listen 443 quic;
    http3 on;
    ssl_early_data on;
}

# Test with curl
$ curl --http3 --tls-earlydata https://cdn.example.com/

Frequently Asked Questions

0-RTT (zero round-trip time), or early data, is a TLS 1.3 handshake mode: a client holding a pre-shared key from an earlier connection sends encrypted application data in the first flight, before the handshake completes. QUIC reuses it. Early data is replayable, so only safe methods belong in it.

# TLS 1.3 0-RTT in Nginx
server {
    listen 443 ssl;
    ssl_protocols TLSv1.3;
    ssl_early_data on;  # Enable 0-RTT
    
    # Protect against replay attacks
    proxy_set_header Early-Data $ssl_early_data;
    # Backend can check this header and reject
    # non-idempotent requests with Early-Data: 1
}

# QUIC 0-RTT (automatic with HTTP/3)
server {
    listen 443 quic;
    http3 on;
    ssl_early_data on;
}

# Test with curl
$ curl --http3 --tls-earlydata https://cdn.example.com/

Yes. 0-RTT is also known as Early Data, Zero Round-Trip Time Resumption, 0RTT. 0-RTT (zero round-trip time), or early data, is a TLS 1.3 handshake mode: a client holding a pre-shared key from an earlier connection sends encrypted application data in the first flight, before the handshake completes. QUIC reuses it. Early data is replayable, so only safe methods belong in it.

Related CDN concepts include: