HTTP response code for POST when resource already exists
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.
Return 409 Conflict when a POST request attempts to create a resource that already exists. This is the most semantically correct HTTP status code for this scenario, as it tells the client exactly why the request failed and implies the conflict can be resolved (for example, by choosing a different identifier). While 422 Unprocessable Entity and 200 OK are sometimes used, 409 Conflict is the widely accepted standard for duplicate resource creation attempts.
Why 409 Conflict Is the Right Choice
RFC 9110 (which supersedes RFC 7231) defines 409 as follows: "The request could not be completed due to a conflict with the current state of the target resource." A POST that tries to create a user with an email that already exists is a textbook example. The conflict is between the request payload and the existing state of the resource collection.
The response body should explain what caused the conflict and, ideally, point the client to the existing resource so it can decide whether to update it instead.
Alternative Status Codes and When They Apply
Different teams and APIs use different codes for this scenario. Here is a breakdown of the options, their correctness, and when each one makes sense:
| Status Code | Name | When to Use | RFC Basis |
409 Conflict | Conflict | Resource already exists, client can resolve | RFC 9110, Section 15.5.10 |
422 Unprocessable Entity | Unprocessable Entity | Valid syntax but semantic errors (validation failures) | RFC 9110, Section 15.5.21 |
200 OK | OK | Server ignores the duplicate and returns the existing resource | RFC 9110, Section 15.3.1 |
204 No Content | No Content | Server ignores the duplicate, nothing to return | RFC 9110, Section 15.3.5 |
303 See Other | See Other | Redirect client to the existing resource | RFC 9110, Section 15.4.4 |
400 Bad Request | Bad Request | Malformed request (not the right fit for duplicates) | RFC 9110, Section 15.5.1 |
409 vs. 422: The Common Debate
422 Unprocessable Entity means the request body is syntactically valid but semantically wrong. It is the right choice for validation errors like "email format is invalid" or "age must be positive." But a duplicate resource is not a validation error. The data itself is valid; the problem is a conflict with existing state. Use 422 for validation failures, 409 for state conflicts.
200 OK for Idempotent POST
Some APIs intentionally treat POST as idempotent. If the resource already exists, they return 200 OK with the existing resource. This design choice is valid when the client does not care whether it created the resource or it already existed:
The already_existed flag in the body lets the client distinguish between a new creation and a pre-existing resource.
303 See Other: The Redirect Approach
303 See Other tells the client "the resource you're trying to create already exists, go look at it here." This is semantically elegant but rarely used in modern JSON APIs because API clients do not typically follow redirects automatically for POST requests:
Implementation Examples
Express.js / Node.js
Spring Boot / Java
Django / Python
PUT vs. POST: Different Semantics for Duplicates
PUT and POST have different semantics for existing resources:
- POST creates a new resource. If it already exists, the server should reject the request (409) or handle it as idempotent (200).
- PUT creates or replaces a resource at a specific URL. If the resource exists, PUT updates it. There is no "conflict" because replacement is the intended behavior.
If your API needs "create or update" behavior, use PUT with the resource identifier in the URL. If your API needs strict "create only" behavior, use POST and return 409 on duplicates.
Race Conditions and Concurrency
In high-concurrency systems, two requests can both check for existence, find no duplicate, and both attempt to create the resource. Handle this with database-level uniqueness constraints:
The database uniqueness constraint is the source of truth. The application-level check is a performance optimization (avoids a database write on obvious duplicates), not a correctness guarantee.
Response Body Best Practices
A good 409 response should include enough information for the client to take action:
Do not just return 409 with an empty body or a generic "Conflict" message. The client needs to know which field caused the conflict and what to do about it.
Common Pitfalls
Using 400 Bad Request for duplicates. A 400 means the request itself is malformed (bad JSON, missing required fields). A duplicate resource is a perfectly well-formed request that conflicts with existing state. These are fundamentally different error categories.
Returning 409 without identifying the conflict. A bare 409 Conflict response with no body forces the client to guess what went wrong. Always include the conflicting field and a human-readable message.
Not handling race conditions. Checking for existence and then inserting is not atomic. Without a database uniqueness constraint, two concurrent POSTs can both pass the existence check and create duplicate records.
Using 409 for validation errors. "Email format is invalid" is a 422, not a 409. Reserve 409 for conflicts with existing server state, not for input validation failures.
Inconsistent behavior across endpoints. If /api/users returns 409 for duplicates but /api/products returns 422, clients must learn different error handling for each endpoint. Pick one convention and apply it consistently across your API.
Summary
Use 409 Conflict when a POST request attempts to create a resource that already exists. This status code clearly communicates that the request was understood but cannot be fulfilled due to a conflict with existing state. Include the conflicting field and a link to the existing resource in the response body. Back your uniqueness logic with database constraints to handle concurrency. Reserve 422 for validation errors, 200 for intentionally idempotent POST endpoints, and 400 for malformed requests.
Related reading
- http server respnd with an output from an async function
- HTTP URL Address Encoding in Java
- HttpClient and using proxy - constantly getting 407
- HttpListener class with HTTPS support
- https on S3 WITHOUT cloudfront possible?
- httptrace endpoint of Spring Boot Actuator doesn't exist anymore with Spring Boot 2.2.0
- I want to get the host MAC address in kubernetes pod where that pod runing on
- I want to know the size of bounding box in object-detection api

System Design Fundamentals
Build a strong foundation in designing scalable, reliable distributed systems.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.