Skip to content

HTTP status code · Client errors (4xx)

409 Conflict

409 Conflict means the request could not be applied because it clashes with the current state of the resource, and the client may be able to resolve the clash and try again. The most common causes are creating something that already exists (a duplicate email or name) and saving an edit based on an outdated version.

Facts about this status code
Class4xx, Client errors
Defined inRFC 9110 §15.5.10
Cacheable by defaultOnly with explicit Cache-Control or Expires
Safe to retryAfter resolving the conflict, for example by fetching the latest version and reapplying the change
Relevant headers
  • ETag: return the current version so the client can re-read and retry

What 409 means

RFC 9110, section 15.5.10, says 409 is for conflicts with the current state of the target resource that the user might be able to resolve, and that the response should explain the conflict well enough for the user to recognize its source. Its example is a PUT carrying changes that clash with an earlier edit made by someone else.

Real systems use it that way. Kubernetes answers 409 when you update an object with a stale resourceVersion ("the object has been modified; please apply your changes to the latest version") and when you create one that already exists. Elasticsearch answers 409 on version conflicts, and Amazon S3 answers 409 BucketAlreadyExists when a bucket name is taken.

Use 409 for state conflicts, not for invalid input. A malformed email is a 400 or 422; an email that is valid but already registered is a 409, because the same request would succeed against a different state.

Common causes

If you are visiting the site

  • You tried to sign up with an email or username that is already registered.
  • Someone else edited the same record while you had it open, and your save was based on the old version.
  • You double-clicked a submit button and the second request tried to create the same thing again.

If you run the server

  • A unique constraint in the database (email, slug, external ID) rejected an insert, and the API maps it to 409.
  • Optimistic concurrency: the client sent a version number or ETag that no longer matches the stored one.
  • A state machine transition that is not allowed right now, such as cancelling an order that already shipped.
  • Git-style operations, such as pushing or merging onto a branch that has moved.

How to fix it

If you are visiting the site

  • Reload the page to get the latest version, redo your change and save again.
  • For sign-ups, use a different email or try logging in or resetting the password on the existing account.

If you run the server

  • Return a body that says what conflicted (which field, which current version) so the client can resolve it without guessing.
  • On clients, handle 409 by re-reading the resource, merging or asking the user, then retrying with the new version.
  • Use idempotency keys on create endpoints so a double submit returns the first result instead of a conflict.
  • Catch unique-constraint violations explicitly; letting them bubble up turns a clean 409 into a 500.

How to send 409

Express (Node.js)
app.put('/documents/:id', (req, res) => {
  res.set('ETag', '"v8"');
  res.status(409).json({ error: 'Document was changed by someone else', current_version: 8 });
});
Next.js App Router route handler
// app/documents/[id]/route.ts
export async function PUT() {
  return Response.json(
    { error: 'Document was changed by someone else', current_version: 8 },
    { status: 409, headers: { 'ETag': '"v8"' } }
  );
}
Go net/http
mux.HandleFunc("PUT /documents/{id}", func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("ETag", `"v8"`)
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusConflict) // 409
	w.Write([]byte(`{"error":"Document was changed by someone else","current_version":8}`))
})
Python FastAPI
from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

@app.put("/documents/{id}")
def update_document(id: str):
    return JSONResponse(
        status_code=409,
        content={"error": "Document was changed by someone else", "current_version": 8},
        headers={"ETag": '"v8"'},
    )

Commonly confused with

409 vs 412
412 is the precondition version: the client sent If-Match and the ETag did not match. 409 covers conflicts the server detects without conditional headers.
409 vs 422
422 means the input itself is invalid; 409 means valid input that clashes with what already exists.
409 vs 400
400 is for requests that are malformed regardless of state; a 409 request would succeed against a different server state.

Frequently asked questions

Should a duplicate email return 409 or 400?
409 fits better: the request is valid, but it conflicts with an existing record. Some teams prefer 422 or 400 to avoid revealing which emails are registered, which is a reasonable privacy trade-off on sign-up forms.
What is the difference between 409 and 412?
412 Precondition Failed is triggered by a conditional header such as If-Match not matching. 409 is the server detecting a conflict with the current state on its own, for example through a version field in the body.
Can a client retry a 409 automatically?
Not blindly: the same request will conflict again. Retry after re-reading the resource and reapplying the change, which some clients do automatically for simple counters or merges.
Why does Kubernetes return 409 Conflict?
Either the object already exists on create, or you updated it with a resourceVersion that is no longer current because someone else changed it. Fetch the latest object, reapply your change and update again.

Last reviewed by Arielton Oberek.