Skip to content
Control Plane Labs

425 Too Early

Client error response defined by RFC 8470 §5.2.

Last updated July 27, 2026

Common causes at a glance

  • A non-idempotent POST or PUT sent as TLS 1.3 early data on a resumed session
  • A CDN forwarding early data to an origin that declines to process it
  • An origin behind a proxy that sets Early-Data: 1 on the forwarded request
  • A client library enabling 0-RTT globally without distinguishing safe methods

Reproduce it

curl -i --tlsv1.3 --tls-early-data -X POST -d 'amount=100' https://api.example.com/v1/transfers

What 425 Too Early means

TLS 1.3 lets a client resuming a session send application data in its very first flight, before the handshake finishes. That saves a round trip, which is why 0-RTT is attractive on mobile networks. The catch is that this data has no replay protection: anyone who captures those bytes can send them again, and the server cannot distinguish the copy from the original.

For a GET of a public page nobody cares. For a POST that moves money, a replay is a duplicate transaction. 425 is the server declining to gamble.

What the spec says

RFC 8470 §5.2 defines 425 and states that a client must not automatically retry unless the retry can be sent where early data is not in play. §5.1 defines the Early-Data header a reverse proxy sets to tell an origin the request arrived in early data, so the origin can answer 425 even though it did not terminate TLS. §3 gives the server rules and §4 the client rules. The underlying anti-replay problem is RFC 8446 §8, and 0-RTT itself is §2.3. A 425 must not be cached.

What actually causes it

Almost nobody enables 0-RTT by hand; it arrives with a CDN. Edge providers can accept early data and forward it with Early-Data: 1, at which point an origin framework with replay protection answers 425. The application team, who never opted into anything, sees sporadic 425s on write endpoints and nothing on reads. The second pattern is a client library that turns on early data for every request regardless of method. Early data is only defensible for safe, idempotent requests: a retried GET costs nothing, a retried POST /transfers costs a customer money. Sticky sessions do not save you, because the defences in RFC 8446 §8 are per-server and a replay can land on another node.

How to debug it

Confirm early data is actually in play before touching application code. curl only sends it with TLS 1.3 and a cached session ticket:

curl -sS --tlsv1.3 --tls-early-data -o /dev/null -w '%{http_code}\n' \
https://api.example.com/v1/transfers

On the origin, look for the Early-Data request header. If it is present, the CDN is forwarding 0-RTT and the origin is right to reject it. The fixes are upstream: disable early data at the edge for write paths, restrict it to GET and HEAD, or have the edge hold it until the handshake completes. On the client, retry once and never loop.

Working examples

curl

# Early data needs TLS 1.3 plus a resumed session, so warm the ticket first.
curl -sS --tlsv1.3 https://api.example.com/v1/transfers -o /dev/null

curl -sS -i --tlsv1.3 --tls-early-data \
  -X POST -d 'amount=100' \
  https://api.example.com/v1/transfers

# Check whether the origin is seeing the proxy's marker:
curl -sS -H 'Early-Data: 1' -i https://origin.example.com/v1/transfers

Python (requests)

import requests

# requests/urllib3 do not expose TLS early data, so you will not produce a 425
# from Python directly. What you can do is handle it correctly when a proxy does.
payload = {"amount": "100"}
url = "https://api.example.com/v1/transfers"

resp = requests.post(url, data=payload, timeout=10)
if resp.status_code == 425:
    # Retry exactly once; the second request rides the completed handshake.
    resp = requests.post(url, data=payload, timeout=10)
print(resp.status_code)

Node (fetch)

const url = "https://api.example.com/v1/transfers";
const body = "amount=100";
const opts = {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body,
};

let res = await fetch(url, opts);

// One retry only. Looping on 425 is how you build an infinite replay loop.
if (res.status === 425) res = await fetch(url, opts);
console.log(res.status);

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 4xx codes