HTTP 409 Conflict

Conflict

HTTP 409 is Conflict: the request can’t be completed because it clashes with the current state of the resource. RFC 9110 section 15.5.10 meant it for conflicts the user “might be able to resolve”, and asks the server to send enough in the body for the user to see what clashed. Most 409s answer a PUT, PATCH or POST.

What sends it

  • Creating something that already exists: a taken username, a duplicate slug. In PostgreSQL that’s error 23505, unique_violation.
  • Updating from a stale copy. The Kubernetes API answers 409 when an update’s resourceVersion is out of date (“the object has been modified; please apply your changes to the latest version and try again”), and when you create a name that’s already taken.
  • Document stores: Elasticsearch and CouchDB answer 409 for version conflicts.
  • State machines: cancelling an order that has shipped, deleting a folder that isn’t empty.

409 or its neighbours

body doesn't parse                                400
parses, but a field breaks a rule                 422 (or 400)
valid, but clashes with what's stored             409
If-Match sent, and the ETag has moved on          412 Precondition Failed
update had to be conditional, and wasn't          428 Precondition Required

For optimistic locking, HTTP has its own mechanism: the client sends back the ETag it read in If-Match, and the server answers 412 when it no longer matches (RFC 9110 section 13.1.1). 428 comes from RFC 6585 and tells a client it must send that condition. 409 fits when the version travels in the body, or when the clash isn’t about versions at all. Search 409 412 in the tool below to compare the two.

A body worth sending

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

{"type": "https://example.com/probs/order-shipped", "title": "Order already shipped",
 "status": 409, "detail": "Order 4182 shipped on 2026-10-12 and can't be cancelled", "state": "shipped"}

A client shouldn’t resend a 409 unchanged. It should fetch the current state, merge or ask the user, and try again; Kubernetes controllers do exactly that on a resourceVersion conflict, often through client-go’s retry.RetryOnConflict. 409 isn’t heuristically cacheable.

In your stack

try {
  await db.query('INSERT INTO projects (slug) VALUES ($1)', [slug]);
} catch (err) {
  if (err.code === '23505') return res.status(409).json({ error: 'slug_taken' });   // node-postgres
  throw err;
}
try:
    Project.objects.create(slug=slug)
except IntegrityError:                                       # Django
    return JsonResponse({"error": "slug_taken"}, status=409)
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" { // pgx
    http.Error(w, "slug taken", http.StatusConflict)
    return
}

Try it in the HTTP Status Codes

409 Conflict

  1. 409Conflict4xx Client error

    The request clashes with the current state of the resource: a duplicate key, a version that has moved on, or two edits in flight at once.

    When to use it

    Unique constraint violations, and optimistic locking where the client’s version no longer matches what is stored.

    Common mistakes

    Sending it for field validation, which is 422, and leaving the client with no way to see the state it collided with.

    Not cacheable by defaultRFC 9110

Every code, by class

Codes marked unofficial come from one server’s own convention — nginx, Cloudflare, cPanel, Microsoft — and are not in the IANA registry.

Runs in your browser. Nothing you enter is sent anywhere.