412 Precondition Failed: Stale ETag, Fix If-Match
412 Precondition Failed means your If-Match ETag no longer matches the server version. Spot the stale write and CDN ETag trap. Free instant check, no sign-up.
Check your domain for this issue now
Free, no sign-up. Runs the exact check this guide describes and shows what to fix.
Problem
A conditional PUT, PATCH, or DELETE comes back 412 Precondition Failed. The request was valid, auth passed, and the route works. But you attached a condition — an If-Match ETag or an If-Unmodified-Since date — and the server tested that condition against the resource’s current state and it came back false. Someone or something changed the resource since you last read it.
Symptoms
- A save fails with “the record was modified by someone else” or “version conflict — please refresh.”
- A
PUTwithIf-Match: "..."returns 412 while a plainPUT(no condition) succeeds and overwrites. - Preconditions pass against the origin directly but fail the moment traffic goes through a CDN or reverse proxy.
- A create-only
PUTwithIf-None-Match: *returns 412 because the resource already exists.
What 412 Actually Means
RFC 9110 (§15.5.13) defines 412 as the case where “one or more conditions given in the request header fields evaluated to false when tested on the server.” The key word is conditions: 412 never happens by accident. It only appears when your request carried a precondition header, and the server is telling you the precondition didn’t hold.
This is the machinery of optimistic concurrency control, and it exists to solve exactly one problem: the lost update. You GET a resource and the server hands back ETag: "v1". You edit it. You PUT your change with If-Match: "v1", which means “only apply this if the resource is still the version I read.” If a second client saved "v2" in the gap, the server’s ETag is now "v2", your precondition fails, and you get 412 — instead of silently stomping the other person’s work. That 412 is the system working. It caught a race you’d otherwise never have noticed.
The spec pins down the evaluation order too (§13.2.2): If-Match and If-Unmodified-Since take precedence over If-None-Match and If-Modified-Since. That ordering matters when a request carries more than one condition, and it’s why a stale If-Match short-circuits everything else. The whole design is a promise: no unsafe write lands unless the version you targeted is still the version that’s there.
Top 3 Causes
-
A stale ETag from a genuine race - The textbook case, and usually correct behavior. You read version
"v1", someone else wrote"v2"while you were editing, and yourIf-Match: "v1"no longer matches. The server refuses to clobber the newer data and returns 412. There is no bug here — the fix is to re-GET, take the current ETag, merge your change onto the fresh state, and resubmit. If you find yourself annoyed by this 412, remember what it prevented: your edit quietly erasing someone else’s. -
A proxy rewriting the ETag - The infuriating case. Apache’s
mod_deflateappends-gzipto ETags when it compresses a response, so the client caches"abc-gzip"while the origin’s canonical ETag is"abc". YourIf-Matchsends the mangled value, the origin compares it against the real one, and it never matches — 412 on every conditional write, even with zero concurrent activity. Some CDNs regenerate or strip ETags entirely. The tell is that preconditions work against the origin and break through the edge. -
Date-based preconditions and clock skew -
If-Unmodified-Sincecompares timestamps at one-second granularity, which is coarse and fragile. If the resource was modified in the same second you read it, or the server’sLast-Modifiedclock drifts from the client’s, the comparison can fail even when nothing meaningful changed. ETags are strong identifiers; HTTP dates are a lossy approximation. When you can, condition onIf-Matchwith an ETag rather thanIf-Unmodified-Sincewith a date.
Diagnose with DechoNet
- HTTP Check to see the
ETagandLast-Modifiedheaders the endpoint actually returns to a cleanGET. A 412 is a mismatch between the validator you’re sending and the validator the resource currently has, so the first move is to read the current one. If the ETag the check reports doesn’t match what your client has cached, that’s your answer — you’re conditioning on a version that’s gone. If the check shows no ETag at all, the endpoint may not support conditional requests the way your client assumes.
Resolution Checklist
- Compare the ETag you’re sending in
If-Matchagainst the ETag the resource returns right now. If they differ, the resource moved — re-read before you write. - For a real stale-write race, re-
GETthe resource, take the fresh ETag, reapply your change onto the current state, and resubmit with the newIf-Match. Never loop the same stale condition — it will 412 forever. - If you’re behind Apache with compression, check for the
-gzipETag suffix. Either disable the suffix (DeflateAlterETag NoChangeon modern Apache) or strip it client-side before sendingIf-Match. - If preconditions pass at the origin but fail through a CDN, inspect what ETag the edge is serving versus what the origin sets. A proxy that regenerates ETags breaks conditional requests by design — key it to pass the origin’s value through untouched.
- Prefer
If-Matchwith a strong ETag overIf-Unmodified-Sincewith a date. Second-granularity timestamps cause spurious 412s that an entity tag would avoid. - Re-run the conditional request with the current validator and confirm it now returns 2xx.
When to Escalate
- If clients hit 412 on every conditional write with no concurrency, the ETag pipeline is broken somewhere between origin and client — usually a proxy rewriting or stripping the tag. That’s an infrastructure fix, and it belongs with whoever owns the edge.
- If a resource that’s meant to support optimistic concurrency returns 412 on legitimate first writes, check whether an upstream cache is serving a stale ETag from a previous version. Cached validators that outlive the resource state turn every conditional write into a false conflict.
Related Tools
Related Guides
Share this guide