How HTTP Works
4xx · Client error
< HTTP/1.1 409 Conflict

409 Conflict

409 Conflict means the request clashes with the resource's current state: duplicates, stale versions, locks. Use ETags and If-Match, and handle retries.

Reviewed 6 min readintermediate4 sourcesTry itMarkdown
Cacheable
Only with explicit freshness
Retry?
After resolving the conflict
Usually sent by
Origin app
Spec
RFC 9110 §15.5.10
Often confused with
412, 422
On this page

TL;DR: 409 Conflict means your request conflicts with the resource’s current state. Read the error body, fetch the current resource, and resolve the duplicate, stale version or blocked operation before retrying.

RFC 9110 section 15.5.10: the request “could not be completed due to a conflict with the current state of the target resource”, and the response should include enough information for the user to recognize the source of the conflict. Because the state can change, the client can usually fix it and resubmit. If the failure is a failed precondition header (If-Match, If-Unmodified-Since), the precise code is 412 Precondition Failed; many APIs use 409 for version-field conflicts in the body.

The wire exchange below is illustrative.

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

{
  "type": "https://api.example.com/problems/duplicate-email",
  "title": "Email already registered",
  "status": 409,
  "detail": "A user with email ana@example.com already exists.",
  "existing": "/users/8821"
}

What you see in your client

These are the client formats for a response carrying 409 and the reason phrase Conflict; the URLs are placeholders. They are derived from library source, rather than captured server output. A server-supplied reason phrase can change the Python, .NET and Spring text.

  • Axios: AxiosError: Request failed with status code 409. With the default status validation, the promise rejects; inspect error.response.status, error.response.data and error.response.headers. Axios constructs this message in settle.
  • fetch: the promise resolves when the HTTP response arrives. response.status === 409 and response.ok === false; check those before reading a success payload. A network failure rejects separately. MDN documents this distinction.
  • Python requests: requests.exceptions.HTTPError: 409 Client Error: Conflict for url: https://api.example.com/resource. This comes from response.raise_for_status(), not from requests.get() alone. Save the body before raising if it contains useful diagnostics. Requests source.
  • curl -f: curl: (22) The requested URL returned error: 409. curl -i shows headers and the error body without fail mode; --fail-with-body keeps the body while returning a failing exit code. curl source, option documentation.
  • .NET: System.Net.Http.HttpRequestException: Response status code does not indicate success: 409 (Conflict). This is the English message from EnsureSuccessStatusCode(); inspect the HttpResponseMessage first when you need its body. Runtime message template.
  • Spring: WebClientResponseException.Conflict with message 409 Conflict from GET https://api.example.com/resource for WebClient.retrieve(). RestTemplate’s default error handler uses HttpClientErrorException.Conflict; its message can include the response body. These class names follow the Spring 6.2 WebClient source and HTTP client source.

Who sent it?

Start with the response body and the application log for the same request. These messages identify a conflict source; database exceptions become HTTP 409 only when an application maps them:

EvidenceSource
duplicate key value violates unique constraint "users_email_key" (Postgres 23505), E11000 duplicate key error (MongoDB), Duplicate entry ... for key (MySQL 1062)Database unique constraint mapped to 409
ConditionalCheckFailedExceptionDynamoDB conditional write mapped by your API; DynamoDB itself returns HTTP 400
{"message":"Conflict"} or Kubernetes Operation cannot be fulfilled on deployments.apps "x": the object has been modified; please apply your changes to the latest version and try againKubernetes API optimistic concurrency (resourceVersion)
BucketAlreadyOwnedByYou, BucketNotEmpty, OperationAbortedS3; creating an already-owned bucket in us-east-1 is a documented 200 exception
Docker Conflict. The container name "/web" is already in useDocker Engine API
WebDAV MKCOL into a missing parentWebDAV servers
GitHub 409 Conflict: merge conflict or “Git Repository is empty.”GitHub API

Fix it, most common first

  1. Duplicate create. The resource already exists. GET it and use it, or switch to an upsert (PUT to a client-chosen id is idempotent). For retried POSTs, reuse an Idempotency-Key where the API implements that contract; consult its key lifetime and payload-matching rules.
  2. Stale update (lost-update protection). Another writer changed the resource. Re-fetch, merge, and resend with the new ETag in If-Match (server answers 412 on mismatch) or the new version field.
  3. Invalid state transition. Cancelling a shipped order, deleting a non-empty folder, deploying while a deploy is in progress. Read the current state and offer an allowed action.
  4. Locked or in-use resource. Another operation holds it. Retry with backoff, or use 423 Locked semantics if you control the API.
  5. Concurrent deploys or infrastructure changes. Kubernetes resourceVersion mismatches: retry the read-modify-write loop (client-go’s retry.RetryOnConflict).

Common causes by stack

  • Express + PostgreSQL: the handler below maps SQLSTATE 23505 (unique_violation) to 409. Check err.constraint to distinguish a duplicate email from a duplicate id; return an actionable conflict rather than exposing the database message. PostgreSQL error codes.
  • Django/DRF: a custom 409 exception or handler reports an IntegrityError or a version conflict. Inspect the handler and database constraint together; a serializer validation error follows a different path. DRF exception handling.
  • Spring Boot: an application exception handler maps a duplicate or an optimistic-lock failure to 409. Reload the entity before retrying an update; keep the @Version check in the persistence operation. Spring optimistic-lock exception.
  • Kubernetes API: an update carries an old metadata.resourceVersion. GET the object again, apply your intended change to that version, and retry the update. This is an API concurrency conflict, separate from an ingress returning a response from your application. Kubernetes updates.
  • Amazon S3: a bucket operation conflicts with existing state, or a concurrent delete races with a conditional write. Read the XML error Code: BucketNotEmpty calls for inspecting the bucket’s contents, while a conditional-write 409 Conflict has operation-specific retry rules. For CompleteMultipartUpload, AWS requires a new multipart upload after this conflict. S3 errors, conditional-write rules.

A DynamoDB ConditionalCheckFailedException in the application log is a useful clue, but AWS assigns it HTTP 400. If your client sees 409, inspect the API’s translation of that failure rather than attributing the HTTP status directly to DynamoDB.

Server-side patterns

Unique constraint to 409 (Express and Postgres)

app.post('/users', async (req, res) => {
  try {
    const user = await db.users.insert(req.body)
    res.status(201).location(`/users/${user.id}`).json(user)
  } catch (err) {
    if (err.code === '23505') {            // Postgres unique_violation
      return res.status(409).json({ error: 'duplicate', constraint: err.constraint })
    }
    throw err
  }
})

Let the unique constraint decide. A SELECT followed by an insert leaves a race: two concurrent requests can both pass the check.

Optimistic locking with ETag

GET /articles/7 HTTP/1.1

HTTP/1.1 200 OK
ETag: "v12"

PUT /articles/7 HTTP/1.1
If-Match: "v12"
Content-Type: application/json

HTTP/1.1 412 Precondition Failed

With a version column instead of headers, update with WHERE id = $1 AND version = $2 and return 409 when zero rows change. Require the header with 428 Precondition Required if you want clients to always send it.

Django and Spring

Django REST Framework supports a custom APIException subclass with status_code = 409; catching IntegrityError still requires you to identify the violated constraint and return that response. In Spring, add an @ExceptionHandler with @ResponseStatus(HttpStatus.CONFLICT) for the conflicts your API exposes. Spring can translate persistence failures to DataIntegrityViolationException or OptimisticLockingFailureException; these exception classes alone do not select an HTTP response. DRF exception customization, Spring exception handlers.

Client handling

Resolve the conflict described in the response before resending. For version conflicts, re-GET, compare your original copy with the new copy, merge or ask the user, then resubmit once. For “already exists” on a create that you retried, treat it as success if the existing resource matches what you sent.

Reproduce and verify

curl -si -X POST https://api.example.com/users \
  -H 'Content-Type: application/json' -d '{"email":"ana@example.com"}'   # second run => 409

curl -si https://api.example.com/articles/7 | grep -i '^etag'
curl -si -X PUT https://api.example.com/articles/7 \
  -H 'If-Match: "stale-version"' -H 'Content-Type: application/json' -d '{"title":"x"}'

409 vs its neighbors

  • 400: the request is malformed. 409: it is valid but conflicts with state.
  • 412: a precondition header you sent evaluated false.
  • 422 Unprocessable Content: semantic validation failed regardless of other resources’ state.
  • 423: the resource is locked (WebDAV).
  • 404: the target does not exist.

Experiment in the request builder.

Frequently asked questions

What does 409 Conflict mean?

The request is well formed but cannot be completed because it conflicts with the current state of the target resource, such as a duplicate unique value or a stale version (RFC 9110 section 15.5.10). The client can usually resolve the conflict and resubmit.

What is the difference between 409 and 412?

412 Precondition Failed means a precondition header you sent, such as If-Match or If-Unmodified-Since, evaluated false. 409 is a state conflict detected by the server's own logic, such as a duplicate or invalid transition.

Should a duplicate create return 409 or 422?

409 is the common choice for a unique-constraint violation because the conflict is with existing data. 422 fits when the request is invalid on its own terms, independent of other resources.

How do I implement optimistic locking over HTTP?

Return an ETag on GET, require it in If-Match on PUT or PATCH, and reject mismatches with 412. Alternatively store a version number and update with WHERE version = ?, returning 409 when no row matched.

Is it safe to retry a request that returned 409?

Not unchanged. Re-read the resource, merge or choose a resolution, then send an updated request. For retried creates, an Idempotency-Key header can return the original result when the API supports it.

Sources

  1. MDN Web Docs: 409 Conflictdeveloper.mozilla.org
  2. RFC 9110 section 15.5.10: 409 Conflictrfc-editor.org
  3. RFC 9110 section 13.1.1: If-Matchrfc-editor.org
  4. RFC 9457: Problem Details for HTTP APIsrfc-editor.org

Keep going

Browse /search