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/transfersWhat 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
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request