Resource library

QA How-To

HTTP 409 conflict: What Testers Need to Know

Learn HTTP status 409 conflict semantics, reproduce concurrency and state collisions, and design reliable API tests for recovery, retries, and data integrity.

21 min read | 3,193 words

TL;DR

HTTP 409 Conflict is appropriate when a syntactically valid request cannot be completed because it clashes with current resource state. Strong tests create that state deterministically, assert the conflict contract, prove no partial side effects occurred, and verify the intended recovery path.

Key Takeaways

  • A 409 response means the request conflicts with the current state of the target resource or system.
  • Reproduce the conflicting state deliberately instead of treating any duplicate-looking request as a valid 409 case.
  • Verify atomicity because a rejected request must not leave partial writes, emitted events, or consumed quotas.
  • For optimistic concurrency, assert validators, stale updates, fresh retries, and competing writers.
  • Return machine-readable conflict details that help a client refresh, merge, rename, or choose another action.
  • Test conflict recovery separately from blind automatic retry, which can repeat the same invalid state forever.

The http status 409 conflict response tells a client that its request cannot be completed because it clashes with the current state of the target resource or the wider system. The request may be valid JSON and use the correct method, yet still be impossible to apply safely at that moment.

For QA engineers, 409 is a state-model problem. You need to arrange the precondition that creates the collision, send the request at the right time, verify a useful response, and prove that rejected work did not corrupt data. Duplicate names, stale versions, mutually exclusive transitions, and concurrent writes are common examples, but they are not interchangeable.

TL;DR

Situation Likely result Core test question
Malformed JSON 400 Could the server parse the request?
Missing authentication 401 Does the caller need to authenticate?
Valid request, stale resource version 409 or 412 by contract Was a concurrency precondition violated?
Valid create, unique name already used 409 Does current state block the requested result?
Semantically invalid field 400 or 422 by contract Is the representation itself invalid?
Unexpected database or application failure 500 Did the server fail rather than identify a client-resolvable conflict?

Do not assert 409 from the error message alone. Verify the prior state, the exact operation, the post-state, and the documented way the caller can resolve the conflict.

1. HTTP Status 409 Conflict Semantics

A 409 response is used when a request conflicts with the current state of a resource. The server should provide enough information for a human or client to recognize the source of the conflict when doing so is safe. The important idea is state: if the same request could succeed after the client refreshes, changes a unique value, waits for a transition, or reconciles versions, 409 may fit.

Typical examples include creating a workspace with an already reserved slug, updating a document from a stale revision, publishing an item whose required dependency was archived, or moving an order from shipped back to draft. A batch can also conflict when one requested member makes the complete atomic operation impossible.

The status does not define one universal error schema or recovery action. API designers must document a domain error code, relevant fields, current version where safe, and actionable next step. Testers should resist reverse engineering that contract from one happy-path failure.

The target matters too. A uniqueness collision can be modeled as a conflict with the collection receiving a POST, even though no new member was created. A stale update can be modeled using conditional requests and might use 412 Precondition Failed instead. The API specification is the oracle, provided it expresses coherent HTTP and business semantics.

2. When 409 Is Correct, and When It Is Not

409 is not a generic label for every rejected request. If the server cannot parse JSON, 400 is clearer. If a field violates a declared schema or domain rule independent of current state, many APIs choose 400 or 422. If authentication is absent, 401 applies. If the caller lacks permission, 403 or a concealed 404 may apply. If an explicit If-Match precondition evaluates false, 412 is often the precise choice.

Ask a diagnostic question: What change could make the same basic intent valid? If the answer is changing current server state or reconciling with it, conflict semantics are plausible. If the answer is fixing the request grammar, credentials, or permission, another status is likely stronger. If the answer is repairing an unavailable database, a 5xx code belongs to the server failure.

Condition 409 fit? Reason
Username already allocated Often Current collection state blocks uniqueness
Email has invalid syntax No Representation validation failure
Two writers use the same old revision Yes Requested update conflicts with current version
If-Match value is stale Often 412 An explicit HTTP precondition failed
Account is locked by a workflow Possibly Current domain state blocks transition
SQL deadlock leaks through the API Usually no Implementation failure is not automatically a client conflict

Use API contract testing with Pact when consumer behavior depends on exact conflict codes and fields.

3. Build a State Model Before Writing Tests

A reliable 409 test starts with states and transitions. For an order, list draft, confirmed, paid, packed, shipped, cancelled, and refunded. Draw which commands are legal from each state. Then annotate whether an illegal transition produces 409, is idempotently accepted, or uses another domain response.

State models expose missing cases. Can two users confirm the same draft? Can cancellation race with shipment? Can a refund begin while payment capture is pending? Does an administrative override follow different rules? Does a timed background transition alter the result between setup and action?

Keep setup observable. Create the fixture through a supported API, read it back, and capture its ID and version. Avoid relying on shared seeded records that other parallel tests modify. When direct database setup is necessary, encapsulate it and verify that it mirrors production invariants. An impossible fixture can make a conflict test pass for the wrong reason.

For each rejected transition, specify invariants. Order status remains unchanged. Inventory reservation is not duplicated. The ledger has no extra entry. No customer notification is sent. The audit trail records only the rejected attempt if policy requires it. A 409 status without these checks proves very little about transaction safety.

4. Duplicate Creation and Uniqueness Conflicts

Duplicate creation is the most familiar 409 scenario, but subtle choices affect the test. Uniqueness can be case-sensitive, tenant-scoped, locale-aware, normalized, or time-bound. QA-Team, qa-team, and qa%2Dteam might resolve to the same canonical slug. Leading spaces, Unicode normalization, soft-deleted records, and reserved words can also affect allocation.

Start by creating one resource and recording the complete response. Submit a second POST that collides on the documented key. Assert 409, a stable domain code such as slug_already_exists, and a field pointer when promised. Then query the collection and prove only one logical record exists. Check that the original was not overwritten and the losing request did not allocate an orphaned child or consume a paid entitlement.

Test tenant boundaries. The same slug may be legal in two organizations but illegal twice within one. Test case and normalization rules explicitly, including the value displayed to the client. Avoid exposing another tenant's resource identifier in conflict details.

Also clarify idempotency. Two requests with the same idempotency key and same payload may be a replay that returns the stored success, not a uniqueness conflict. The same key with a different payload may be rejected as a conflict. Those cases need separate fixtures and assertions. The API idempotency testing guide covers that distinction in depth.

5. Optimistic Concurrency with ETags and Versions

Optimistic concurrency lets clients read a version, edit locally, and submit an update that is accepted only if the resource is still at that version. HTTP validators can support this design. A GET returns an ETag; a client sends If-Match with that value on PUT, PATCH, or DELETE. If another writer changed the representation, the precondition is false. Many APIs use 412 for that explicit HTTP failure, while some domain-version designs return 409. Test the documented choice consistently.

The canonical scenario uses two clients:

  1. Client A and client B read version 7.
  2. Client A updates successfully and creates version 8.
  3. Client B submits its edit based on version 7.
  4. Client B receives the declared conflict or precondition response.
  5. Client B refreshes version 8, reconciles its change, and retries with the new validator.

Assert that weak and strong ETags are handled according to the endpoint's semantics. Do not strip quotation marks from an ETag unless the client library explicitly requires it. Verify missing If-Match behavior when conditional updates are mandatory. Test wildcard preconditions only if the API supports them.

Most importantly, prove the stale write did not replace client A's data. Read the final representation and its audit history. A correct 409 body paired with a lost update is still a critical failure.

6. Runnable Node.js Concurrency Test

The following self-contained JavaScript file uses only current Node.js built-in APIs. It starts a small versioned HTTP service, sends two updates from the same initial version, and proves that one wins while the other receives 409. Save it as conflict.test.mjs and run node --test conflict.test.mjs on a modern Node release.

import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { once } from 'node:events';

test('a stale version receives 409 without overwriting data', async (t) => {
  let record = { id: 'doc-1', version: 1, title: 'Original' };

  const server = http.createServer(async (request, response) => {
    if (request.method === 'GET' && request.url === '/documents/doc-1') {
      response.writeHead(200, { 'content-type': 'application/json' });
      response.end(JSON.stringify(record));
      return;
    }

    if (request.method === 'PUT' && request.url === '/documents/doc-1') {
      const chunks = [];
      for await (const chunk of request) chunks.push(chunk);
      const proposed = JSON.parse(Buffer.concat(chunks).toString('utf8'));

      if (proposed.version !== record.version) {
        response.writeHead(409, { 'content-type': 'application/problem+json' });
        response.end(JSON.stringify({
          title: 'Version conflict',
          status: 409,
          currentVersion: record.version
        }));
        return;
      }

      record = { ...record, title: proposed.title, version: record.version + 1 };
      response.writeHead(200, { 'content-type': 'application/json' });
      response.end(JSON.stringify(record));
      return;
    }

    response.writeHead(404).end();
  });

  server.listen(0, '127.0.0.1');
  await once(server, 'listening');
  t.after(() => server.close());

  const address = server.address();
  const baseURL = `http://127.0.0.1:${address.port}`;
  const firstRead = await fetch(`${baseURL}/documents/doc-1`).then(r => r.json());

  const update = title => fetch(`${baseURL}/documents/doc-1`, {
    method: 'PUT',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ version: firstRead.version, title })
  });

  const [a, b] = await Promise.all([update('Writer A'), update('Writer B')]);
  assert.deepEqual([a.status, b.status].sort(), [200, 409]);

  const finalRecord = await fetch(`${baseURL}/documents/doc-1`).then(r => r.json());
  assert.equal(finalRecord.version, 2);
  assert.ok(['Writer A', 'Writer B'].includes(finalRecord.title));
});

The server is illustrative, but the test shape transfers to a real API. Create one resource, read one version, issue competing operations, assert the result set rather than assuming which request wins, and inspect final state.

7. Race Conditions and Deterministic Concurrency Tests

A test that fires two requests with Promise.all creates overlap, but it does not prove both reached the critical point together. Fast local execution may serialize them. For important races, add a test-only synchronization hook below the public interface, use database locks you can observe, or coordinate workers at a barrier. Keep such controls out of production behavior or protect them rigorously.

Do not assert which named client wins unless priority is part of the contract. Assert allowed outcome sets: one success and one conflict, exactly one durable state, no duplicate dependent rows, and a consistent version increment. Run the scenario repeatedly as a stress supplement, but retain a deterministic integration test for diagnosis.

Expand beyond two identical writes. Race rename against delete, cancel against ship, membership removal against permission update, and two allocations against the final unit of capacity. Vary arrival order around the transaction boundary. Test across application instances and a production-like database isolation level because an in-memory single-process test can hide distributed races.

Capture request IDs, timestamps from one trusted clock, transaction identifiers where available, and final reads. If a failure occurs only under load, those artifacts help establish whether the API returned two successes, a late 500, or a correct 409 with incorrect side effects.

8. Validate the 409 Error Contract

A useful conflict response supports a safe next action. Assert Content-Type, numeric status, stable domain code, concise title, resource reference, and current version or conflicting field where disclosure is allowed. If the API uses Problem Details, verify its documented type URI or identifier and extensions. Do not bind tests to punctuation in human-readable prose unless copy is itself a requirement.

For a stale edit, a response might expose currentVersion and advise refresh. For a duplicate slug, it might identify /slug as the conflicting field and suggest that the client choose another value. For a workflow lock, it might include a safe state code and an allowed-actions link.

Negative contract checks matter. The body must not leak SQL constraint names, table names, stack traces, another tenant's data, or internal hostnames. A raw database message such as a unique-index exception is unstable and can reveal implementation. It also forces clients to parse vendor-specific text.

Check localization boundaries. Machine fields stay stable while user-facing detail may change by language. Correlation IDs should match logs and traces but must not be reused as conflict identifiers. Confirm headers and body do not disagree, for example HTTP 409 paired with body status: 400.

9. Atomicity, Side Effects, and Event-Driven Systems

The most consequential 409 defects happen after the response is chosen. A rejected order update may still decrement inventory, enqueue an email, emit a domain event, or charge a quota. Test the complete unit of work. Read authoritative stores and observable downstream outputs after the conflict.

In event-driven systems, define whether a rejected command produces an audit event, a rejection event, or no event. If it produces one, verify the event contains the right request and version metadata without looking like a successful state change. Consumers must not project a conflicted operation into read models.

Delayed messages introduce another form of stale write. A command accepted into a queue can conflict when processed because state changed in the meantime. The synchronous API might return 202, while job status later becomes conflicted. Test that asynchronous contract rather than expecting an immediate HTTP 409 where the server cannot yet know the outcome. Verify compensation, dead-letter policy, retry classification, and user notification.

Database rollback alone is not enough when external side effects occur outside the transaction. Use an outbox or equivalent reliable pattern if the architecture promises atomic publication. From QA's perspective, assert that only the winning operation produces the success event and that replay does not multiply it.

10. Retry and Client Recovery Behavior

Blindly retrying the same 409 request is usually pointless because the conflicting state has not changed. A client needs a conflict-specific strategy: fetch the latest representation, merge edits, select a new unique value, wait for a documented temporary lock, or ask the user to choose.

Test the whole recovery loop. After a stale update, verify the client refreshes without discarding unsaved local work, presents conflicting fields clearly, and submits the chosen resolution with the latest version. Confirm that canceling the merge does not issue another write. For a duplicate name, keep form fields intact and focus the conflicting input.

Automatic retry can be valid when the client recomputes its command from fresh state. Bound the number of attempts and preserve idempotency protection. Do not apply generic exponential retry middleware to every 409. It can amplify load and create a tight loop that never resolves.

Distinguish 409 from 429 and 503 in resilience logic. Rate limits and temporary unavailability often include retry timing. A state conflict usually requires information or action, not just delay. See API rate limiting testing for retry-window coverage.

11. HTTP Status 409 Conflict Test Matrix

A practical suite should include these layers:

  • Contract: exact status, media type, stable error code, schema, safe detail, and correlation data.
  • State transition: every prohibited edge in the domain model and representative permitted edges.
  • Concurrency: stale writers, simultaneous creates, competing workflow commands, and cross-instance behavior.
  • Integrity: final record, version, child rows, ledger, inventory, quota, audit, and events.
  • Identity: owner, collaborator, unauthorized user, administrator, and tenant isolation.
  • Normalization: case, whitespace, Unicode, canonical URLs, and soft-deleted uniqueness.
  • Recovery: refresh, merge, rename, cancel, and retry with fresh state.
  • Resilience: timeout ambiguity, client replay, idempotency key behavior, and delayed processing.

Prioritize money, inventory, permissions, and irreversible workflows. Pairwise selection can limit the combinatorial set, but every invariant around a high-impact transaction deserves explicit evidence. Record both the losing response and final authoritative state in failure artifacts.

A conflict suite is successful when it can tell the difference between correct rejection, lost update, duplicate side effect, incorrect error classification, and a client that cannot recover. Counting 409 responses alone cannot do that.

12. How to Diagnose Unexpected 409 Responses

Begin with reproducibility. Capture the request method, normalized route, safe headers, payload, actor, resource version, idempotency key, and correlation ID. Read the resource before and after if authorization permits. Identify which layer generated 409: gateway policy, application state machine, persistence adapter, or downstream service.

Check for environmental drift. Parallel automated tests may share an email, slug, or tenant. Old soft-deleted fixtures may retain a unique key. A background workflow may advance state between setup and action. Clock skew can affect leases. Eventually consistent reads can show an old version even after a write succeeds.

Then inspect transaction and event evidence. Was a uniqueness constraint the intended domain guard or an unhandled database detail? Did two requests both pass an application-level existence check before one failed at commit? Did a timeout make the client replay an operation that already succeeded?

Fix the test only when the test owns the collision. Give fixtures unique, traceable values and wait on observable state rather than arbitrary sleeps. If the application exposes an undocumented 409 or leaves partial work, report the defect with both response and integrity evidence.

Interview Questions and Answers

Q: What does HTTP 409 Conflict mean?

It means a valid request cannot be completed because it conflicts with the current state of the target resource or system. I expect the response to identify the conflict safely and support a recovery action. I also verify that rejection is atomic.

Q: How is 409 different from 400 and 422?

400 commonly describes malformed or otherwise invalid requests, and 422 is often used when a well-formed representation fails semantic validation. 409 centers on current state, such as an allocated name or stale version. The API contract should make the boundary consistent.

Q: What is the difference between 409 and 412?

412 means a request header precondition, such as If-Match, evaluated false. 409 is a broader state conflict and is often used with domain versions or workflow rules. For explicit conditional HTTP requests, I usually expect 412 unless the documented API deliberately standardizes on 409.

Q: How do you test optimistic concurrency?

I have two clients read the same version, let one update successfully, then submit the other's stale update. I assert the declared conflict response, confirm the winner's data remains, check the version and audit trail, then test refresh, reconciliation, and a fresh retry.

Q: Why can a 409 test pass while the system is still broken?

The endpoint can return 409 after partially writing data or emitting an event. I verify database state, dependent records, inventory or ledger invariants, messages, notifications, quotas, and audit outcomes. The status is only one observable.

Q: Should a client automatically retry 409?

Not without resolving the state. The client usually must refresh, merge, rename, or choose another transition. An automatic attempt is safe only if it recomputes from fresh state, is bounded, and preserves idempotency.

Q: How do you make a concurrency test deterministic?

I coordinate requests at a known barrier near the critical section or use observable locks in a controlled environment. I assert the allowed outcome set rather than a named winner. I also test across real application instances and the production-like data store.

Common Mistakes

  • Treating 409 as a generic response for validation, authorization, rate limiting, or server failures.
  • Creating the conflict accidentally with shared test data instead of arranging and proving the relevant state.
  • Asserting one 409 response but never checking final data, events, notifications, or ledger effects.
  • Assuming the first launched concurrent request must win, which makes timing-dependent tests flaky.
  • Parsing human-readable database error text instead of a stable domain error code.
  • Retrying an unchanged conflict automatically and creating an expensive infinite loop.
  • Ignoring tenant scope, case folding, Unicode normalization, and soft-deleted uniqueness.
  • Using fixed sleeps to wait for state changes rather than observable completion.

Conclusion

A strong http status 409 conflict test proves that current state truly blocks the requested operation, the response explains that collision safely, and the system preserves every important invariant. That requires more than sending a duplicate payload and checking one number.

Model legal transitions, create controlled competing writers, inspect final state and side effects, and exercise the client recovery path. This approach turns 409 from a vague API error into precise evidence about concurrency and data integrity.

Interview Questions and Answers

Explain HTTP 409 Conflict with an example.

409 means the operation conflicts with current server state. For example, two editors read version 7, one saves version 8, and the second tries to save based on version 7. The service rejects the stale write, preserves the winner, and returns information that supports refresh or merge.

How would you distinguish 409 from a validation error?

I ask whether changing current server state could make the same request intent valid. An allocated slug or stale version is state-dependent and fits 409, while invalid email syntax does not. I verify that the API specification uses stable domain codes so consumers do not infer the distinction from prose.

What would you assert after a rejected conflict?

I assert the status and error contract, then inspect authoritative state. The original record, version, dependent rows, inventory, ledger, quota, audit log, events, and notifications must match the documented atomic outcome. This catches partial writes hidden behind a correct response.

How do you test simultaneous duplicate creation?

I create unique test scope, synchronize two requests for the same canonical value, and assert the permitted result set, commonly one success and one 409. Then I query by the unique key and prove only one resource and one set of side effects exist. I repeat across application instances with the production-like data store.

When is 412 more appropriate than 409?

When an explicit request precondition such as `If-Match` evaluates false, 412 directly expresses that failure. A domain command with an embedded revision or an incompatible workflow state may use 409. I test whichever coherent rule the API documents, including missing and malformed preconditions.

What is a good client recovery flow for a stale edit?

The client fetches the latest representation, preserves local unsaved work, shows conflicting fields, and lets the user merge or cancel. A new submission uses the latest validator or version. Tests should ensure cancel performs no write and automatic attempts are bounded.

Why are sleeps weak in race-condition tests?

A sleep does not prove that requests overlap at the critical section, and timing changes across machines. I prefer a barrier, controllable hook, or observable lock in a test environment. I then assert allowed outcome sets and durable invariants instead of relying on arrival guesses.

Frequently Asked Questions

What causes an HTTP 409 Conflict?

A 409 occurs when a request cannot be applied because it clashes with current resource or system state. Common causes include stale versions, duplicate unique values, and prohibited workflow transitions.

Is HTTP 409 a client error or server error?

409 belongs to the 4xx client error class, but that does not imply malformed syntax. It tells the client that the requested operation conflicts with current state and usually needs reconciliation or a changed action.

Should duplicate POST requests return 409?

They may return 409 when the second request conflicts with a uniqueness rule. If the request carries a valid idempotency key and is an exact replay, the API may instead return the stored result, so test those contracts separately.

What is the difference between HTTP 409 and 422?

409 focuses on conflict with current state. 422 is often used when a well-formed representation contains values that fail semantic validation independent of a competing state, although each API must document its convention.

Can I retry a 409 response?

Retry only after resolving or refreshing the conflicting state. Repeating the identical request blindly often produces the same conflict and can increase load.

How do I test a version conflict?

Let two clients read the same version, allow one update to win, and submit the stale update from the other. Assert the conflict response, verify no lost update or partial side effect, and test a retry using the fresh version.

Should an If-Match failure return 409 or 412?

412 Precondition Failed is the precise HTTP response when an explicit `If-Match` condition evaluates false. Some APIs use 409 for domain-version conflicts, so the documented contract and consistent behavior determine the test oracle.

Related Guides