HTTP 409 vs 412 — Conflict vs Precondition Failed Comparison
http 409 vs 412: both signal state conflicts, but Conflict is for resource-level clashes while Precondition Failed is triggered by If-Match / If-None-Match headers. ETag examples included.
Quick Cheat Sheet
| Aspect | 409 Conflict | 412 Precondition Failed |
|---|---|---|
| Triggered by | The request body / target state | A conditional header (If-Match, etc.) |
| Tied to ETag/If-Match? | Not necessarily | Yes, almost always |
| Use for optimistic locking? | Possible | Standard mechanism |
| Common scenario | Duplicate username, branch conflict | Stale resource version in PUT |
The Real Distinction
Both codes signal that "the request can't proceed because the resource state isn't what you'd need it to be," but the trigger differs.
409 Conflict (RFC 9110 § 15.5.10) is returned when the request itself conflicts with the current state of the target resource — for example, trying to create a username that's already taken, or trying to delete a folder that still contains files. The conflict is intrinsic to the operation.
412 Precondition Failed (RFC 9110 § 15.5.13) is specifically returned when one or more conditional request headers — If-Match, If-None-Match, If-Unmodified-Since, or If-Modified-Since (on non-GET) — evaluate to false. The client explicitly told the server "only proceed if X holds," and X didn't hold.
Optimistic Locking with 412
The textbook use of 412 is optimistic concurrency control:
GET /docs/42 → 200 OK, ETag: "v3"
PUT /docs/42
If-Match: "v3"
...new body...
→ 412 Precondition Failed (someone else updated to v4)
This pattern (used by Google Docs, Notion's API, and most modern collaborative editors) is the canonical 412 use case. Without conditional headers, return 200 / 200 every time and let the lost-update problem happen — or use 409.
When to Use Which
- Use 412 if the client sent a conditional header and the precondition failed.
- Use 409 if there's no conditional header but the operation is semantically impossible (duplicate key, dependent resource exists, branch can't be fast-forwarded).
- Many APIs lazily use 409 for both. That works, but loses the precision that lets clients distinguish "you raced someone" (412 → re-fetch and retry) from "this will never work" (409 → user must change input).
Real-World Examples
- GitHub API returns 409 when a PR can't be merged (branch behind, conflicts) and 412 when an If-Match ETag mismatch occurs on a contents update.
- AWS S3 returns 412 for failed
If-Match/If-None-Matchon object PUTs.
Real-World Use Case
Implementing collaborative editing: client GETs document with ETag, displays it, user edits, client PUTs with If-Match: "<etag>". If another user saved in between, return 412 and the client re-fetches + diff-merges. For username collisions on POST /users, return 409 with a JSON body indicating the conflicting field.
Look Up Any Status Code
Related Comparisons
HTTP 400 vs 422 — Bad Request vs Unprocessable Entity Comparison
http 400 vs 422 explained: when to return Bad Request for malformed syntax versus Unprocessable Entity for valid syntax that fails business validation. Includes API examples.
HTTP 423 vs 409 — Locked vs Conflict Status Code Comparison
http 423 vs 409: 423 Locked specifically signals an explicit lock on the resource, while 409 Conflict is a generic state conflict. WebDAV, file editing, and admin lock examples.
HTTP 304 vs 200 — Not Modified vs OK (Caching) Comparison
http 304 vs 200: 304 is sent when a conditional GET hits an unchanged resource, saving bandwidth. Learn ETag, If-None-Match, and how 304 cuts CDN/origin costs.
HTTP 401 vs 403 — Unauthorized vs Forbidden Status Code Comparison
Confused about http 401 vs 403? Learn the exact difference between Unauthorized and Forbidden, when each is returned, and how Express, Stripe, and GitHub APIs use them.