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.

4xx

412

Precondition Failed

View full 412 page →

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-Match on 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

Browse all status codes →