TLS 1.3 0-RTT and HTTP 425 Too Early: Handling Replay on the Server
TLS 1.3 early data has no guarantee against replay across connections. Based on RFC 8446 and RFC 8470: what 0-RTT does not protect, how the Early-Data header works, and when to send 425.
With TLS 1.3 0-RTT (early data) enabled, a returning client can send its first HTTP request before the handshake finishes. The price is that the request can be copied and resent by an attacker. RFC 8446 says it plainly: for 0-RTT data, “there are no guarantees of non-replay between connections.” This post walks through where that risk comes from and what a server can do about it with the Early-Data header and the 425 Too Early status code from RFC 8470, sticking to what the standards actually say.
How 0-RTT works
Per RFC 8446 Section 2.3, when client and server share a PSK (pre-shared key), the client can send application data in its first flight. The PSK is either provisioned externally or obtained from a session ticket in an earlier handshake. The session-ticket case is session resumption.
Client Server
ClientHello
+ early_data
+ pre_shared_key
(Application Data*) -------->
ServerHello
{EncryptedExtensions}
+ early_data*
{Finished}
<-------- [Application Data*]
(EndOfEarlyData*)
{Finished} -------->
The early data in parentheses is encrypted only with keys derived from the PSK. Not waiting for the ServerHello is where the speed comes from, and also where the weakness comes from.
The * marks messages or extensions that are sent only in some situations. Per RFC 8446 Section 4.5, the client sends EndOfEarlyData only if the server accepted early data by including the early_data extension in EncryptedExtensions; otherwise the client MUST NOT send it. The figure as published in RFC 8446 shows (EndOfEarlyData) without the *. Erratum 8794, which corrects this, has the status “Held for Document Update”, and the figure above incorporates it.
The same section flags 0-RTT data as having weaker security properties than other TLS data:
- It is not forward secret, because it is encrypted solely under keys derived from the offered PSK.
- Ordinary 1-RTT data gets replay protection from the server’s Random value. 0-RTT data does not depend on the ServerHello, so there is no guarantee of non-replay between connections. The RFC notes this matters most when the data is authenticated, whether with TLS client authentication or inside the application protocol.
Some things are guaranteed. The server will not process the same 0-RTT data twice within one connection, and an attacker cannot make 0-RTT data appear to be 1-RTT data, since the two are protected with different keys.
What TLS stops and what it doesn’t
RFC 8446 Section 8 splits the threat into two classes.
- Plain duplication: a network attacker copies a flight of 0-RTT data and sends it again.
- Exploiting client retries: if the server system has zones that don’t share state, an attacker can send the same ClientHello and early data to both zone A and zone B. A accepts and processes the 0-RTT data. B rejects 0-RTT and forces a full handshake. If the attacker blocks A’s ServerHello, the client completes the handshake with B and probably retries the request. The system as a whole processes it twice.
For the first class, the RFC says servers SHOULD share state so that 0-RTT data is accepted at most once, and Sections 8.1 to 8.3 describe how.
- Single-use tickets (8.1): keep a store of valid tickets and delete each one when it is used. In distributed deployments this needs a shared session database, which can make high 0-RTT success rates hard to achieve.
- ClientHello recording (8.2): record a unique value derived from the ClientHello (usually the random value or the PSK binder) within a time window and reject duplicates. A freshly started server SHOULD reject 0-RTT while any part of its recording window overlaps its startup time.
- Freshness checks (8.3): use the ticket age to reject 0-RTT for stale ClientHellos. The RFC says Internet clients imply windows on the order of ten seconds to absorb clock error and measurement variation, and states that freshness checking alone is not sufficient, because it cannot detect replays inside that window.
Separately, the server MUST ensure that each instance (a machine, a thread, or any other entity in the serving infrastructure) accepts 0-RTT for the same 0-RTT handshake at most once. That is a minimum: it caps replays at the number of server instances. It does not eliminate them.
For the second class, the text is direct: it cannot be prevented at the TLS layer and MUST be dealt with by the application. Appendix E.5 adds that the Section 8 mechanisms do not provide complete protection against receiving multiple copies of client data, and lists example attacks:
- Duplicating actions with side effects, such as a purchase or a money transfer
- Storing and replaying 0-RTT messages to reorder them, e.g. moving a delete after a create
- Cache timing: replaying to a different cache node, then measuring latency on a separate connection to learn whether two requests hit the same resource
- With a large number of replays, timing measurements of cryptographic operations or overloading rate-limiting systems
So anti-replay in the TLS stack bounds how many times data can be replayed. Guaranteeing that a request takes effect only once is the application’s job.
RFC 8470: three options for an HTTP server
Section 3 of RFC 8470 lists three ways a server that enables early data can mitigate replay, and says all three are equally effective.
- Reject early data at the TLS layer. TLS only lets a server accept all early data or none, so every request sent in early data is discarded.
- Defer processing until the handshake completes. Only requests on a successfully completed connection get processed. This can be decided per request.
- Respond with 425 (Too Early). For individual requests where the replay risk is judged too high, the client is told to retry without early data.
The key point follows. How much replay a request can tolerate depends on the resource, and only the origin server knows. Method safety is one input, but some resources produce side effects even with safe methods, so it cannot be relied on universally. RFC 8470 therefore RECOMMENDS that origin servers let resources explicitly configure whether early data is appropriate, and says that absent such information, origin servers MUST either reject early data or make sure requests are not processed before the handshake completes.
A request can also arrive partly in early data, with the rest after the handshake. What matters is not when the bytes arrived but when the server starts acting on the request’s contents. Parsing header fields and choosing a route are likely free of side effects. Anything beyond that may not be.
Rules for clients
Under RFC 8470 Section 4, absent other information, clients MAY send requests with safe methods in early data and MUST NOT send unsafe methods, or methods whose safety is unknown. RFC 9110 Section 9.2.1 defines GET, HEAD, OPTIONS, and TRACE as safe.
A client that uses early data MUST retry when it receives 425. Section 5.2 says user agents SHOULD retry automatically, and the retry MUST NOT be sent in early data.
RFC 8446 Appendix E.5 also constrains TLS implementations. They MUST NOT enable 0-RTT unless the application specifically asks for it, and MUST NOT automatically resend rejected 0-RTT data unless the application says so. They MUST also give the application a way to tell whether the handshake has completed. That last requirement is what makes the “defer” strategy implementable on the server side.
Behind a proxy or CDN: the Early-Data header
When TLS terminates at a CDN or reverse proxy, the origin cannot see whether a request came in early data. The gateway, for its part, has no idea whether the request is safe to replay. The Early-Data header and 425 from RFC 8470 Section 5 bridge that gap.
GET /resource HTTP/1.1
Host: example.com
Early-Data: 1
The rules:
- An intermediary that forwards a request before its TLS handshake with the client completes MUST send it with
Early-Data: 1, and MUST NOT remove the header if it is already present. - The only valid value is
1. Multiple or invalid instances MUST be treated by the server as a single instance with value1. - The header is for intermediaries. A user agent sending a request in early data does not need to add it, since sending in early data already implies it understands 425 and is willing to retry.
- Waiting for the handshake to complete does not make an
Early-Data: 1request safe at the origin, because it was already sent in early data on a previous hop. If it cannot be processed safely, it MUST be rejected with 425. - The header MUST NOT appear in responses or request trailers.
Gateways have obligations too (Section 6.1). A gateway MUST NOT forward requests received in early data unless it knows the origin understands Early-Data and will correctly generate 425. If it is unsure, it SHOULD either hold the request until its client handshake completes or answer 425 itself. When a 425 comes back, an intermediary MUST forward it if the request it received and forwarded already carried Early-Data. If the intermediary itself received the request in early data, it MAY retry on its own, but only after the TLS handshake on that connection completes.
Handling it at the origin
The following is framework-neutral pseudocode. The core idea is to keep an explicit allowlist of routes that may be processed from early data.
# Only routes the resource owner has judged safe to process from early data
EARLY_DATA_OK = {
("GET", "/api/catalog"),
("GET", "/static/app.js"),
}
# request.tls.used_early_data is a hypothetical placeholder, not a real API.
# True if any byte of this request was received in TLS early data.
def handle(request):
via_early_data = (
request.headers.get("Early-Data") is not None # early data on a previous hop
or request.tls.used_early_data # this server terminates TLS
)
if via_early_data and (request.method, request.route) not in EARLY_DATA_OK:
return Response(status=425) # client retries without early data
return route(request)
A few notes:
request.tls.used_early_datais a placeholder. In practice your TLS stack has to record, per request, whether any of that request’s bytes were received as early data, and expose that to the application. Don’t substitute “the handshake is still incomplete when the handler runs.” A request sent in early data can sit in a buffer until the handshake completes, can arrive partly in early data and partly after the handshake (Section 3), or, with out-of-order delivery, can arrive after the handshake completes (Section 6.4). In each case that condition is false and a replay-sensitive request gets routed normally.- Since odd values of
Early-Datamust be treated as1, checking for presence is more correct than comparing the value. - If this server terminates TLS itself, deferring until the handshake completes is an alternative to 425. Handshake-completion state is then used only to decide when deferred processing may resume, not to detect early data. For requests carrying
Early-Data: 1, deferring does nothing. - A server SHOULD NOT send 425 unless the request arrived in early data or carries
Early-Data: 1. Other clients are not guaranteed to retry. - 425 is not cacheable by default.
- Keep state-changing routes out of the allowlist even when they use GET: view counters, coupon issuance, consuming a magic login link.
Operational checks
Consistency across instances. RFC 8470 Section 6.2 stresses that for requests that are unsafe to process before the handshake, every server instance, gateways included, has to agree. Disabling early data, deferring, and 425 are equally good, so instances using different methods are still consistent. But a server MUST NOT act on early data before the handshake completes if it and another instance could decide differently about the same data.
Under load. Section 6.3 says a loaded server SHOULD prefer rejecting TLS early data as a whole over accepting it and processing selectively. Sending 503 or 425 often triggers client retries, which can add load.
No origin that benefits. Section 6.1 notes that if no origin supports Early-Data, it is more efficient to disable early data at the gateway. In the RFC’s words, the gateway “expends significant effort for what can at best be a modest performance benefit.”
HTTP/3. RFC 8470 states that its advice also applies to 0-RTT in HTTP over QUIC. In protocols with out-of-order delivery such as QUIC, early data can arrive after the handshake completes, and a server MAY process it then only if it can rely on other instances correctly handling replays of the same requests (Section 6.4).
Why 425 alone is not enough
425 and Early-Data exist to keep requests from being acted on during the early-data window. The second threat in RFC 8446 Section 8, where a client retries over 1-RTT after 0-RTT is rejected while the original zone has already processed the request, is really a general HTTP retry problem. RFC 8446 itself notes that any application whose clients implement retry behavior already needs some kind of anti-replay defense.
For POST endpoints where duplicate execution hurts, such as payments, orders, or transfers, you need application-level deduplication in addition to blocking early data. A common pattern is a client-generated request identifier (often called an idempotency key) that the server stores, returning the first result for any repeat with the same key. That pattern is industry practice, not something RFC 8446 or RFC 8470 defines. The method semantics behind it are covered in HTTP idempotent methods.
Checklist
- Find out whether 0-RTT is on at each TLS termination point: CDN, load balancer, web server.
- If it is, confirm the origin receives
Early-Dataand can return 425. If neither holds, turning early data off is closer to what RFC 8470 advises. - Keep an explicit allowlist of routes that may be processed from early data. Don’t decide by method alone.
- Make sure every instance applies the same rules.
- Deduplicate operations that must not run twice at the application level, regardless of early data settings.
Sources
- RFC 8446, The Transport Layer Security (TLS) Protocol Version 1.3: Sections 2.3, 4.2.10, 4.5, 8, and Appendix E.5 — https://www.rfc-editor.org/rfc/rfc8446 (Section 2.3 figure reflects Erratum 8794 — https://www.rfc-editor.org/errata/eid8794)
- RFC 8470, Using Early Data in HTTP: Sections 1–6 — https://www.rfc-editor.org/rfc/rfc8470
- RFC 9110, HTTP Semantics: Sections 9.2.1 (Safe Methods) and 9.2.2 (Idempotent Methods) — https://www.rfc-editor.org/rfc/rfc9110
RFC 8470 cites RFC 7231 Section 4.2.1 for method safety. RFC 7231 has since been obsoleted by RFC 9110, so this post uses the corresponding RFC 9110 sections. Configuration and defaults for specific servers and CDNs vary by product and version and are not covered here.