Views: 92

409 Conflict: Why Your Request Clashes With State

409 Conflict means the request was valid but fought the resource's current state and lost. Tell a duplicate key from a stale ETag from a real race. 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 request comes back 409 Conflict. The request itself was fine — valid JSON, correct auth, a route that exists — but the server refuses because completing it would collide with the resource’s current state. Nothing is broken. The server is telling you the world isn’t where your request assumed it was.

Symptoms

  • A POST, PUT, PATCH, or DELETE returns 409 while GET on the same resource works.
  • It happens on the second identical request — the first succeeded, the retry conflicts.
  • A save fails with “the record was modified by someone else” or “version mismatch.”
  • git push is rejected as non-fast-forward, or a container registry rejects a tag that already exists — both are 409s under the hood.

What 409 Actually Means

RFC 9110 (§15.5.10) defines 409 as a request that “could not be completed due to a conflict with the current state of the target resource.” Every word of that matters. The request was understood. Authentication passed. The syntax was valid. The server just can’t apply it without contradicting what already exists.

This is the code people reach for when nothing else fits, and that’s exactly the trap. A 400 Bad Request means the request was malformed — you can’t parse it, the JSON is broken, a required field is missing. A 500 means the server itself hit a bug. A 409 is neither: the request is well-formed and the server is working fine. The problem is state. That distinction is the whole diagnosis, because it points you at the data, not at the code or the payload.

The spec adds a line most people skip: a 409 is expected “in response to a PUT request” when the update conflicts, and “the server SHOULD generate content that includes enough information for a user to recognize the source of the conflict.” A good 409 body tells you which field collided. A lazy one just says “Conflict” and leaves you guessing. If you own the API, put the conflicting resource in the body. If you’re consuming one that doesn’t, that missing detail is why you’re reading this.

Top 3 Causes

  1. A duplicate on a unique constraint - The classic. You POST to create a user with an email that already exists, a slug that’s taken, or an idempotency key you already used. The database’s unique index rejects the insert and the API translates that into 409 instead of leaking a 500. The fix isn’t on the server — the resource already exists, so the client should either fetch it or stop creating it.

  2. A stale write in optimistic concurrency - You read a record at version 3, someone else saved version 4 while you were editing, and your PUT still claims version 3. The server compares versions and refuses to clobber the newer data. If the API uses If-Match with ETags you’d get a 412 here; if it does its own version check in the body, you get a 409. Either way the resolution is identical: re-read, merge, resubmit against the current version.

  3. A blocked state transition or dependency - You try to delete a resource that other rows still reference, cancel an order that already shipped, or move a workflow into a state it can’t legally enter from where it is. The operation is valid in the abstract but illegal right now. The server guards its invariants and answers 409. This is the one that hides real business logic, and the fix depends entirely on the domain.

Diagnose with DechoNet

  • HTTP Check to see the exact status line and headers the endpoint returns to a clean request. A 409 is usually tied to a specific write against specific state, so a plain check often comes back 200 or 405 — and that split is the point. If the route answers normally to a neutral request, the 409 lives in your payload versus the server’s current data, not in a broken deployment. If the check itself returns 409 to everything, the conflict is baked into routing or a proxy, not your request body.

Resolution Checklist

  • Read the response body, not just the code. A well-behaved 409 names the conflicting field or resource. If it does, you’re most of the way done — that’s the row that already exists or the version that moved.
  • Decide which of the three shapes you have. Duplicate create, stale update, or illegal transition. They look identical from the status code and need completely different fixes.
  • For a duplicate create, stop retrying and fetch the existing resource instead. If you control the client, add an idempotency key so a re-sent POST returns the original result rather than a 409.
  • For a stale update, re-GET the resource, take the current version/ETag, merge your change onto it, and resubmit. Never loop the same stale write — it will 409 forever.
  • For a blocked transition, check the business rule the server is protecting. Delete the dependents first, or move through the legal intermediate state. The 409 is doing its job here.
  • Confirm it isn’t a 400 or 500 in disguise. If the payload is genuinely malformed it should be a 400; if the server threw an unhandled exception it should be a 500. Some APIs mislabel — an HTTP Check against a known-good request tells you whether the endpoint is healthy at all.

When to Escalate

  • If a POST that should be idempotent returns 409 on every legitimate retry, escalate to whoever owns the API — they likely need an idempotency-key path so mobile clients on flaky networks don’t get punished for a dropped ACK.
  • If 409s spike right after a deploy, suspect a migration that added a unique constraint to a column with existing duplicates, or a concurrency check that’s now firing on normal traffic. That’s a data problem, and it belongs with whoever ran the migration.

Related Tools

Related Guides

Share this guide

[Ad] Guide Detail Inline
← Back to All Guides