Blog

What Actually Happens When Two Requests Race to Create the Same Thing

September 6, 2026Xerat023 min read
Share

A double-click on a "create" button, two browser tabs open to the same form, a mobile app retrying a request it thinks failed while the original is still processing — these all produce the same underlying scenario: two nearly-simultaneous requests, both trying to create what's supposed to be one thing. What happens next depends entirely on whether anyone thought about this case on purpose.

The naive version, and why it's naive

A straightforward "create if it doesn't exist" implementation — check whether a record with this identifier already exists, and if not, create it — has an obvious gap when two requests run that check at nearly the same instant: both check, both find nothing, both proceed to create. You end up with two records where you wanted one, or a database error if there's a unique constraint that only one of the two inserts can actually satisfy (and the other request gets an ugly, unhandled failure instead of the graceful "oh, this already exists" it should have received).

This is a genuine race condition, not a hypothetical one — it requires no unusual timing, no attacker, no unlikely coincidence, just two requests close enough together, which a double-click alone reliably produces.

What actually prevents it

The fix has to happen at the layer that can actually guarantee atomicity, which usually isn't your application code's own logic (checking-then-acting is exactly the two-step pattern that races) but the data layer underneath it:

  • A unique constraint at the database level, so that even if both requests attempt to insert, only one succeeds and the other gets a clean, well-defined conflict error to handle — rather than relying on an application-level check that has a window where two requests can both pass it.
  • Handling that conflict gracefully, returning something like the existing record (or a clear "already exists" response) instead of a raw database error leaking up to the caller.
  • Idempotency keys for the specific case of "the same client, retrying what it thinks might have failed" — a distinct but related problem to two independent actors racing to create the same thing, solved by having the client itself signal "this retry represents the same intended action," not a new one.

Why this is hard to catch by testing normally

A test that creates one thing, sequentially, and checks it was created, will pass regardless of whether the race condition exists — sequential requests never race, by definition. Catching this requires deliberately firing two (or more) requests concurrently and checking that exactly one record resulted, which most manual testing and a lot of automated testing simply never attempts, because it's not the natural way to write a test unless you're specifically thinking about concurrency.

Reproducing this reliably against a real backend means orchestrating genuinely simultaneous requests against real infrastructure, which is more setup than most teams bother with for a single test case. Testing it against a stateful CRUD mock configured to represent the resource in question — firing two concurrent creates at it and confirming your client-side handling of "the second one got a conflict" degrades gracefully — at least confirms the client half of the problem is handled correctly, which is the half most likely to actually be under your control when the dependency is someone else's real backend.

X

Xerat02

Building MockBase.