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.
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.
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.
If-Match header with an ETag returns 412 when it fails, and plain version fields commonly return 409.Network timeouts create their own duplicates: the client never saw the 201, so it sends the request again. Two common answers:
Idempotency-Key header and return the original result for a repeated key.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.
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.
409 with the existing resource in the body.422.400.500.To see the full list, try our searchable HTTP Status Code Reference, or read status codes for a REST API.