HTTP
Response Code
POST Method
Web Development
Server Responses

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.

Practice system design

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.

http
1POST /api/users HTTP/1.1
2Host: api.example.com
3Content-Type: application/json
4
5{
6  "email": "[email protected]",
7  "name": "Alice Smith"
8}
http
1HTTP/1.1 409 Conflict
2Content-Type: application/json
3
4{
5  "error": "conflict",
6  "message": "A user with email [email protected] already exists",
7  "field": "email",
8  "existing_resource": "/api/users/42"
9}

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 CodeNameWhen to UseRFC Basis
409 ConflictConflictResource already exists, client can resolveRFC 9110, Section 15.5.10
422 Unprocessable EntityUnprocessable EntityValid syntax but semantic errors (validation failures)RFC 9110, Section 15.5.21
200 OKOKServer ignores the duplicate and returns the existing resourceRFC 9110, Section 15.3.1
204 No ContentNo ContentServer ignores the duplicate, nothing to returnRFC 9110, Section 15.3.5
303 See OtherSee OtherRedirect client to the existing resourceRFC 9110, Section 15.4.4
400 Bad RequestBad RequestMalformed 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:

http
1POST /api/subscriptions HTTP/1.1
2Content-Type: application/json
3
4{
5  "user_id": 42,
6  "plan": "pro"
7}
http
1HTTP/1.1 200 OK
2Content-Type: application/json
3
4{
5  "id": 99,
6  "user_id": 42,
7  "plan": "pro",
8  "created_at": "2025-01-15T10:30:00Z",
9  "already_existed": true
10}

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:

http
HTTP/1.1 303 See Other
Location: /api/users/42

Implementation Examples

Express.js / Node.js

javascript
1app.post('/api/users', async (req, res) => {
2  const { email, name } = req.body;
3
4  const existingUser = await db.users.findByEmail(email);
5
6  if (existingUser) {
7    return res.status(409).json({
8      error: 'conflict',
9      message: `A user with email ${email} already exists`,
10      existing_resource: `/api/users/${existingUser.id}`
11    });
12  }
13
14  const user = await db.users.create({ email, name });
15  res.status(201).json(user);
16});

Spring Boot / Java

java
1@PostMapping("/api/users")
2public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) {
3    Optional<User> existing = userRepository.findByEmail(request.getEmail());
4
5    if (existing.isPresent()) {
6        Map<String, String> error = Map.of(
7            "error", "conflict",
8            "message", "A user with this email already exists",
9            "existing_resource", "/api/users/" + existing.get().getId()
10        );
11        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
12    }
13
14    User user = userRepository.save(new User(request.getEmail(), request.getName()));
15    URI location = URI.create("/api/users/" + user.getId());
16    return ResponseEntity.created(location).body(user);
17}

Django / Python

python
1from rest_framework import status
2from rest_framework.response import Response
3from rest_framework.views import APIView
4
5class UserCreateView(APIView):
6    def post(self, request):
7        email = request.data.get('email')
8
9        if User.objects.filter(email=email).exists():
10            return Response(
11                {
12                    'error': 'conflict',
13                    'message': f'A user with email {email} already exists',
14                },
15                status=status.HTTP_409_CONFLICT
16            )
17
18        serializer = UserSerializer(data=request.data)
19        serializer.is_valid(raise_exception=True)
20        user = serializer.save()
21        return Response(
22            UserSerializer(user).data,
23            status=status.HTTP_201_CREATED
24        )

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.
http
1# POST: "Create a user" - 409 if email exists
2POST /api/users
3{"email": "[email protected]", "name": "Alice"}
4
5# PUT: "Set this specific user" - replaces if exists, creates if not
6PUT /api/users/[email protected]
7{"name": "Alice Smith"}

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:

sql
-- The database constraint catches what application-level checks miss
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
python
1from django.db import IntegrityError
2
3class UserCreateView(APIView):
4    def post(self, request):
5        serializer = UserSerializer(data=request.data)
6        serializer.is_valid(raise_exception=True)
7        try:
8            user = serializer.save()
9            return Response(UserSerializer(user).data, status=201)
10        except IntegrityError:
11            return Response(
12                {'error': 'conflict', 'message': 'User already exists'},
13                status=409
14            )

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:

json
1{
2  "status": 409,
3  "error": "conflict",
4  "message": "A user with email [email protected] already exists",
5  "conflicting_field": "email",
6  "existing_resource_url": "/api/users/42",
7  "suggestion": "Use PUT /api/users/42 to update the existing user"
8}

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
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track 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.

Practice system design

All Rights Reserved.