pingrobot

HTTP status codes / 409

409 Conflict — HTTP Status Code

· 3 min read

Short answer

409 Conflict means the request could not be completed because it conflicts with the current state of the resource, for example creating something that already exists or saving changes on top of someone else's newer edit. It is an API-level signal that the client can usually resolve and then resubmit. It does not mean the server is unhealthy.

What 409 means

RFC 9110 (Section 15.5.10) defines 409 Conflict as a request that could not be completed because it conflicts with the current state of the target resource. The RFC frames it as a situation the user might be able to resolve before resubmitting, and says the server should include enough information in the response for the user to recognize the source of the conflict.

The RFC's own example is versioning: if a PUT carries changes that conflict with changes someone else made earlier, the server can refuse with 409 and include details that help merge the two.

When you see it

409 comes almost entirely from applications and APIs. Web servers, CDNs and WAFs do not generate it on their own, because they do not know about your data model. Typical situations:

  • Creating a record whose unique key already exists, such as a username, slug or external ID.
  • Updating a document whose version number has moved on since the client loaded it.
  • Changing a resource that is in the wrong state, such as cancelling an order that has already shipped or deleting a non-empty folder.
  • Starting a job or deployment while another one for the same target is still running.
  • Infrastructure APIs, such as orchestration tools, reporting that a named object already exists.

Common causes

  1. Double submissions. A user clicks "Pay" or "Create" twice, or a client retries after a timeout even though the first request succeeded.
  2. Concurrent edits. Two people, or two background workers, update the same record at nearly the same time.
  3. Stale client data. A mobile app holds an old version of an object and saves it later.
  4. Race conditions in queues. Two consumers process the same message and both try to create the same row.
  5. State machine rules that reject transitions which no longer make sense.

How to fix it

As a client, read the response body; a well-designed API explains the conflict. Then reload the current resource, reconcile, and send a fresh request. For creates that might be retried, use the API's idempotency mechanism if it has one, so a retry returns the original result instead of a conflict.

As an API author, make the conflict actionable. Return the current version or the conflicting field:

app.put("/api/docs/:id", async (req, res) => {
  const doc = await db.docs.find(req.params.id);
  if (doc.version !== req.body.version) {
    return res.status(409).json({
      error: "Document was changed by someone else",
      currentVersion: doc.version,
    });
  }
  // ...save and increment version
});

For unique constraints, map the database error to a 409 instead of letting it bubble up as a 500:

try {
  await db.users.insert({ email });
} catch (e) {
  if (e.code === "23505") {   // PostgreSQL unique_violation
    return res.status(409).json({ error: "Email already registered" });
  }
  throw e;
}

If clients send conditional requests with If-Match and an ETag, a failed precondition gets 412 Precondition Failed instead. Reserve 409 for conflicts you detect without a precondition header.

How to check it

Reproduce the conflict by sending the same create request twice and printing both status codes:

for i in 1 2; do
  curl -sS -o /dev/null -w "%{http_code}\n" -X POST \
    -H "Content-Type: application/json" \
    -d '{"slug":"launch-post"}' https://api.example.com/v1/posts
done

A typical result is 201 followed by 409. Drop -o /dev/null to read the error body.

What it means for uptime monitoring

PingRobot's checker sends a GET with the user agent PingRobot/1.0 (+https://pingrobot.io/bot), follows redirects, and counts the final response as up only if its status is 200 to 399. A 409 would be reported as down.

In practice a plain GET should never produce a 409, because reading a resource does not change its state. If a monitor gets one, the URL is probably an action endpoint, such as a link that triggers a job, or the app is mapping unrelated errors to 409. Either way, point the monitor at a read-only page or health endpoint. A rising number of 409s in your own logs is worth watching as a data or concurrency issue, but it is not an availability signal.

Start monitoring free — 10 monitors, no credit card.

Frequently asked questions

Should a duplicate email on sign-up return 409 or 422?

Both are common. 409 emphasizes that the data clashes with existing state, while 422 treats it as a validation failure. Either works if your API documents it and returns a clear error message.

What is the difference between 409 and 412?

412 Precondition Failed is the specific answer when a conditional header such as If-Match evaluates to false. 409 is the broader code for conflicts the server detects on its own, without a precondition header.

Should a client retry a 409 automatically?

Not blindly. Sending the same request again will hit the same conflict. The client should fetch the current state, resolve the difference, and then submit a new request.