Reading:
HTTP Status Code for a Duplicate Record: Use 409 Conflict

HTTP Status Code for a Duplicate Record: Use 409 Conflict

Metamug
HTTP Status Code for a Duplicate Record: Use 409 Conflict

When a client creates a record that already exists, most APIs let the database throw a unique-constraint error and return 500 Internal Server Error. That is the wrong signal. The server did not fail. The client sent something that clashes with what is already stored, and the client can fix it. Return 409 Conflict.

Which status code for a failed write: 400, 422, 409, 412 or 500

Why 409 and not the others

RFC 9110 defines 409 as a request that conflicts with the current state of the target resource. A duplicate key is exactly that.

Code Means Fits a duplicate?
400 Bad Request The request is malformed or unparseable No. The request is fine.
422 Unprocessable Content Well formed, but fails a validation rule Not quite. It says the data is invalid, not that it collides with existing state.
409 Conflict Clashes with the current state of the resource Yes.
412 Precondition Failed An If-Match style precondition is false Only for conditional requests.
500 Internal Server Error The server failed unexpectedly No. It blames the server and hides a client-fixable error.

The distinction matters to callers. A client that sees 4xx knows retrying the same payload will not help, and that it should change something. A client that sees 5xx assumes a transient server problem and may retry blindly, which is pointless for a duplicate and can hammer your API.

What a good 409 response contains

Tell the caller what conflicted and where the existing record lives, so they can recover without guessing.

POST /users HTTP/1.1
Content-Type: application/json

{"email": "ada@example.com", "name": "Ada"}
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

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

The body follows the problem details format (RFC 9457), which gives clients a stable, machine-readable error shape.

Other places 409 fits

  • Version conflicts. An update based on a stale copy of the resource. For strict optimistic concurrency, an If-Match header with an ETag returns 412 when it fails, and plain version fields commonly return 409.
  • Concurrent operations. A state transition that is still running. Here the conflict is transient, so a client can retry after a short delay. That is the one case where retrying a 409 is reasonable.
  • Invalid state changes. Deleting a resource that still has dependants, or cancelling an order that has already shipped.

Making creates safe to retry

Network timeouts create their own duplicates: the client never saw the 201, so it sends the request again. Two common answers:

  • Accept an Idempotency-Key header and return the original result for a repeated key.
  • Use PUT to a client-chosen id, which is idempotent by definition.

Some APIs return 200 with the existing record for a repeated create instead of 409. That is a valid choice if you want the create to be idempotent. Pick one behaviour and document it.

What about 303 See Other?

303 redirects the client to another resource. It can point at the existing record, but it does not tell the client its request was rejected, so it should not replace the error.

Summary

  • Duplicate or state clash: 409 with the existing resource in the body.
  • Invalid but non-conflicting data: 422.
  • Malformed request: 400.
  • Unexpected server failure: 500.

To see the full list, try our searchable HTTP Status Code Reference, or read status codes for a REST API.



Icon For Arrow-up
Comments

Post a comment