409 Conflict: what it means and how to fix it

409 Conflict means the request is valid, but it conflicts with the current state of the resource on the server. Creating something that already exists, saving changes to a record someone else updated after you read it, or deleting a resource other records still depend on are all 409s. The fix is usually to fetch the current state, reconcile, and try again.

HTTP/1.1 409 Conflict
Content-Type: application/json

{"error": "already_exists", "message": "A user with email [email protected] already exists", "existing_id": "usr_42"}

Common causes

  • Duplicate creation. A unique email, username, slug or external ID is already taken.
  • Concurrent edits. Optimistic locking: the client sends a version number or updated_at that no longer matches the stored one. Kubernetes returns 409 when an update's resourceVersion is stale.
  • Idempotency key in use. Stripe and similar APIs return 409 if a second request reuses an idempotency key while the first one is still running.
  • Invalid state transition. Refunding a payment that was already refunded, or merging a pull request that has merge conflicts.
  • Dependent resources. Deleting a project that still has active resources inside it.

409 vs similar codes

CodeUse it when
409 ConflictThe request clashes with the server's current state.
412 Precondition FailedA conditional header (If-Match, If-Unmodified-Since) didn't match. This is the standard code for ETag-based concurrency.
422 Unprocessable ContentThe data is invalid on its own, whatever the server state. See 400 vs 422.
428 Precondition RequiredThe server requires a conditional request and the client didn't send one.

How to fix a 409 as a client

  1. Read the body. A good API says what conflicts and often returns the ID of the existing resource.
  2. If it already exists, use it. For "create if missing" logic, treat 409 as success and fetch the existing record, or switch to an upsert endpoint if the API has one.
  3. If it was modified, refetch and retry. Get the latest version, reapply your change on top, and send it with the new version or ETag. Don't overwrite blindly.
  4. If an idempotent request is in flight, wait and retry with the same key. You'll get the original request's result.

409 and duplicates on a webhook endpoint

Webhooks are delivered at least once. The same event can arrive twice when your endpoint processed it but timed out before answering, when a provider retries, or when someone replays it. Your handler needs to be idempotent.

The tempting response to a duplicate is 409. Don't. Most providers treat any non-2xx response as a failed delivery and retry it, so a 409 produces more duplicates, and it can push the endpoint toward being flagged as failing. Answer a duplicate with 200 and skip the work.

The standard pattern:

  1. Take the provider's unique event or delivery ID (Stripe's event.id, GitHub's X-GitHub-Delivery, the webhook-id header in Standard Webhooks).
  2. Insert it into a table with a unique constraint, or a cache with a TTL, in the same transaction as the work.
  3. If the insert conflicts, it's a duplicate: return 200 and stop.

If you'd rather drop duplicates before they reach your app, a Webhook Relay function can do it with the built-in KV store: if kv.has(id) is true, call r.stopForwarding() so the duplicate is logged but not delivered; otherwise store the ID with a TTL that covers the provider's retry window. KV operations aren't atomic, so two copies arriving at the exact same moment can both pass. Keep a unique constraint in your database as the final guard. The retries and idempotency guide covers the full design.

When to return 409 from your own API

Return 409 when a request would succeed against a different server state: a unique field already in use, a stale version, an invalid state transition, or an idempotency key whose first request is still running. Include enough detail in the body (what conflicts, the existing ID, the current version) for the client to resolve it automatically. Use 412 if you implement ETags with If-Match.

Frequently asked questions

What does 409 Conflict mean?

HTTP 409 Conflict means the request couldn't be completed because it conflicts with the current state of the target resource. Typical examples are creating something that already exists, updating a record someone else changed since you read it, or deleting a resource that other records still depend on.

How do I fix a 409 Conflict error?

Read the response body to see what conflicts. If the resource already exists, fetch and use it instead of creating it again. If it was changed since you read it, fetch the latest version, reapply your change and retry. If another request with the same idempotency key is still running, wait and retry.

What is the difference between 409 and 422?

422 means the data itself is invalid regardless of what's on the server, such as a malformed email address. 409 means the data would be fine on its own but conflicts with the server's current state, such as a username that's already taken or a stale version number.

Should a webhook endpoint return 409 for a duplicate event?

No. Return 200 for a duplicate you've already processed. Most providers treat any non-2xx response as a failure and keep retrying, so answering a duplicate with 409 creates more duplicates. Record processed event IDs and acknowledge repeats without processing them again.

Why does Stripe return 409?

Stripe returns 409 when a request conflicts with another one, most often when a second request reuses an idempotency key while the first request with that key is still in progress. Wait for the first request to finish, then retry with the same key to get its stored result.