Saltar al contenido principal

Guías / Códigos de estado HTTP explicados

409 Conflict: what it means and how to fix it

A 409 Conflict means the server understood the request, but it cannot carry it out because it conflicts with the current state of the target resource, for example creating something that already exists or saving over a newer version. RFC 9110 uses it for cases where the user might be able to resolve the conflict and send the request again.

What 409 Conflict means

RFC 9110, section 15.5.10, defines 409 as the answer to a request that could not be completed because of a conflict with the current state of the target resource. The code is meant for situations where the user might be able to resolve the conflict and resubmit. The server SHOULD send content that explains the source of the conflict. The RFC names a PUT request as the most likely place for a conflict: if the server tracks versions, and the PUT carries changes that conflict with an earlier change from someone else, the server can answer 409 and put in the body what the client needs to merge the two versions.

The status is in the 4xx class, but the request itself is not the problem. Its syntax is fine and its values are valid. What is wrong is the match between the request and the state of the resource at that moment. That has a practical result: the same request can succeed later, after the state changes or after the client changes what it sends. A 422 is different, because the same invalid data fails every time.

How the error appears

Browsers have no special page for 409. It is mostly an API response behind a button or a form, so people meet it as a message such as "already exists", "conflict" or "this item was changed by someone else". Developers see it as a red row with status 409 in the Network panel of the browser's developer tools. The response body usually carries the explanation. From the command line:

curl -i -X POST https://api.example.com/v1/users   -H "Content-Type: application/json"   -d '{"email": "[email protected]", "name": "Ann"}'

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

{"type": "https://api.example.com/problems/email-in-use",
 "title": "Email already registered",
 "status": 409,
 "detail": "A user with this email address already exists."}

The body follows RFC 9457, "Problem Details for HTTP APIs", which defines the application/problem+json format. A GET request that returns 409 is rare. When a 409 response has an empty body, Chrome shows its generic "This page isn't working" page with the text "HTTP ERROR 409".

409 vs 400, 412, 422 and 428

Five status codes answer "the request is wrong in some way", and some APIs mix them up. The differences come from RFC 9110, and from RFC 6585 for 428.

StatusWhat the server is sayingTypical triggerWhat the client does
400 Bad RequestI cannot process this: malformed syntax or invalid message framing.Broken JSON, bad headers, a corrupt request line.Fix the request format.
409 ConflictThe request is valid, but it conflicts with the current state of the resource.A duplicate create, a stale version in the body, deleting a folder that is not empty.Read the state, resolve the conflict, send again.
412 Precondition FailedA condition in your request headers evaluated to false.If-Match or If-Unmodified-Since no longer matches; If-None-Match: * on an existing resource.Fetch the current version and its new ETag, then retry.
422 Unprocessable ContentThe syntax is right, but I cannot process the instructions inside.A value fails validation or a business rule.Change the data. The same data fails again.
428 Precondition RequiredThis server requires a conditional request.A PUT without If-Match on an API that demands one, to prevent lost updates.Add the conditional header.

To choose between them: if the request can never succeed as written, use 400 or 422. If it is correct but depends on a state that is wrong right now, use 409. If the client explicitly said "only do this if the resource is still at version X", use 412.

What causes a 409 Conflict

  • Creating something that already exists. A unique email, username, slug, order number or file name is taken.
  • Saving over a newer version. Two people, or two browser tabs, edit the same record, and the second save is based on an old copy.
  • An action the current state does not allow. Canceling an order that has already shipped, publishing a post that is already published, or deleting a folder that still has files in it.
  • A missing parent. Creating a file or folder inside a folder that does not exist. WebDAV servers answer this way.
  • Duplicate or parallel requests. A double click on a submit button, or a retry after a timeout when the first request had in fact succeeded.
  • A name taken in a shared namespace. Bucket names, repository names and project names are unique across a whole service.

Where you meet 409 in practice

These are documented cases from real systems. They show how much the meaning of "conflict" varies from one API to the next.

  • WebDAV (RFC 4918). MKCOL returns 409 when intermediate collections are missing, and the server must not create them automatically. A PUT that would create a resource without a parent collection must fail with 409, and COPY and MOVE use it for a missing destination parent.
  • Amazon S3. CreateBucket returns 409 with BucketAlreadyExists when the name is taken, because the bucket namespace is shared by all users. It also returns 409 with BucketAlreadyOwnedByYou when you already own the bucket, in every Region except North Virginia, where it returns 200 OK instead.
  • Kubernetes API. An update that carries a stale resourceVersion gets 409 Conflict. The API server uses this to detect lost updates.
  • CouchDB. 409 means a document with that ID already exists, or the revision you sent is not the latest one.
  • Stripe. 409 means the request conflicts with another request, perhaps because the same idempotency key is in use.
  • GitHub. Merging a pull request through the API returns 409 if you passed a sha and the pull request head does not match it. The same endpoint uses 405 when the merge cannot be performed and 422 for validation failures.
  • GitLab. 409 means a conflicting resource already exists. Its API uses 412 for a failed If-Unmodified-Since on delete.

How to fix a 409 if you are the client

  1. Read the response body. A good API says what conflicts: which field, which version, which existing resource.
  2. For a duplicate, fetch the existing resource. If it is the same thing you tried to create, your goal is already met, so treat the call as done. If it is a different thing, choose another name or key.
  3. For an edit conflict, fetch the current version. Merge your change into it, or apply your change again on top of it, then send the request with the new version number or ETag. Never overwrite blindly, because that discards the other person's work.
  4. For a state conflict, change the state first. Create the parent folder, finish or cancel the pending operation, or empty the folder, and then repeat the request.
  5. Stop repeated submits. Disable the submit button after the first click. For payment-style requests, send an idempotency key so that a retry is recognized as the same request.
  6. Retry only after something changed. A blind retry of the same request usually gets the same 409. The exception is a conflict that comes from a lock or an operation in progress, where a short wait and one retry can work.

If you are a visitor in a web app, the steps are shorter. Reload the page and check whether your item already exists or someone else changed it. Do not press the button again and again. If you edit together with other people, copy your text, reload, and apply it on top of the new version. If the error stays, tell the site's support what you did and at what time.

How to return 409 correctly if you build the API

A 409 is only useful if the client can tell what conflicts and what to do. These patterns cover the usual cases.

Duplicate create

Let the database enforce uniqueness, and translate its error. Checking first with a SELECT and inserting afterwards leaves a gap in which two parallel requests both pass the check. This example uses Express and the pg package; PostgreSQL reports a unique violation with the code 23505 (MySQL uses error 1062, SQL Server uses 2627 and 2601):

app.post('/v1/users', async (req, res, next) => {
  try {
    const { rows } = await db.query(
      'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING id',
      [req.body.email, req.body.name]
    );
    res.status(201).location(`/v1/users/${rows[0].id}`).json({ id: rows[0].id });
  } catch (err) {
    if (err.code === '23505') {   // unique_violation
      return res.status(409).type('application/problem+json').json({
        type: 'https://api.example.com/problems/email-in-use',
        title: 'Email already registered',
        status: 409,
        detail: 'A user with this email address already exists.',
      });
    }
    next(err);
  }
});

On public sign-up forms, this response tells the caller which addresses have accounts. If that matters for your product, respond in the same way whether or not the address exists, and send the real outcome by email.

Version conflicts

Give every editable record a version number and make the update conditional on it:

UPDATE documents
SET    body = $1, version = version + 1
WHERE  id = $2 AND version = $3;

When the statement changes zero rows, someone else saved first. Return 409 with the current version in the body, so the client can merge. Use this pattern when the version travels in the request body.

Conditional requests with ETag

The standard way to prevent lost updates uses HTTP's own headers. The server sends an ETag, the client returns it in If-Match, and a mismatch is a 412, not a 409:

GET /v1/documents/42
HTTP/1.1 200 OK
ETag: "7"

PUT /v1/documents/42
If-Match: "7"

HTTP/1.1 412 Precondition Failed

To make every client send a condition, answer a PUT that has no If-Match with 428 Precondition Required. Real APIs do not all follow this split. Kubernetes answers a stale resourceVersion with 409, and GitLab answers a failed If-Unmodified-Since with 412. Choose one rule, apply it everywhere and document it.

The response body

Use the problem details format: a type URI that identifies the kind of conflict, a short title, the status, and a detail that names the field, version or resource. Add the ID or URL of the conflicting resource when the caller is allowed to see it. RFC 9110 asks for exactly this: enough information for a user to recognize the source of the conflict.

How it looks from the outside

A monitoring check normally reads a page or a health endpoint, and those do not conflict with anything, so a 409 in a check result is unusual and worth an alert. Three patterns are common:

  • A check that creates something. If a check sends a POST with a fixed payload, it passes once and then gets 409 on every run, because the item exists now. Use unique values per run, or check a read-only endpoint.
  • 409 after a deploy. A new version of the app may enforce a uniqueness or state rule that the old one did not. Compare the check result before and after the release.
  • Different results from different locations. When data is replicated or only eventually consistent, two locations can see different state and get different status codes. Compare the results by location before you suspect the network.

The HTTP check requests a URL from several locations and reports the status code for each, so you can see at once whether the 409 is the same everywhere. HostTracker runs checks from 300+ checkpoints in 158 cities and sends alerts by email, SMS, voice call, Slack, Telegram and more when a check fails.

Frequently asked questions

Is 409 a client error or a server error?

It is in the 4xx class, so the response says that the request cannot succeed as it was sent. The server is healthy. What needs to change is usually the state of the resource, for example an existing record that must be fetched or removed, or the data the client sends.

Should a duplicate email return 409 or 422?

Both are used. 409 fits when the cause is an existing resource: the request is fine, but its target state conflicts with it. 422 fits when the value breaks a rule that does not depend on other records, such as a malformed address. Pick one for uniqueness conflicts and apply it to the whole API.

Is an edit conflict 409 or 412?

If the client sent If-Match or If-Unmodified-Since, RFC 9110 points to 412. If the conflict is found from a version number in the body or from the record's state, 409 is the usual answer. The section on conditional requests above names real APIs that make each choice.

Should I retry a request that got a 409?

Not without changing something. Fetch the current state, resolve the conflict, then send the request again. A retry without any change normally gets the same answer. A conflict caused by a lock or a running operation can pass after a short wait.

Can DELETE or GET return 409?

DELETE can: deleting a folder that still has files, or a record that other records depend on, is a state conflict. A GET rarely does, because reading changes nothing. If a GET returns 409, look at how the server handles that request.

Does a PUT to an existing URL cause a 409?

No, not for that reason alone. PUT replaces the resource at that URL, so "it already exists" is not a conflict. A 409 appears if the PUT conflicts with something else, such as a newer version, a missing parent or a unique value used elsewhere.

Can a 409 get a page removed from Google?

If a page that crawlers fetch returns 409, it will not be indexed. Google's documentation says it does not index URLs that return a 4xx status code and removes already indexed ones. This matters for pages, not for API endpoints that search engines never request.

Sources and further reading

Compruébalo ahora

Ejecuta la comprobación gratuita en tu propio sitio, sin necesidad de cuenta.

HTTP check

Monitoriza esto de forma permanente

Recibe un aviso en cuanto algo falle: HostTracker comprueba desde más de 300 ubicaciones y te avisa por correo, SMS, Slack, Telegram y más.

Funciones de HostTracker

Más en esta sección: Códigos de estado HTTP explicados