Why Claude Code Writes Race Conditions
Claude Code hands me a clean booking endpoint. It reads well, the tests pass, I ship it β and a week later production breaks in a way my machine never did. This is the Claude Code race condition problem, and it’s not a bug the linter catches. It’s the gap between code that looks correct and code that survives real traffic. An agent optimizes for code that is correct in isolation, because isolation is all it can see. Concurrency correctness lives in the space between requests, invisible in a single-threaded demo.
The Demo Is Where Confident-Looking Code Lies
I built a normal Express API with Postgres and Prisma, then asked Claude Code for a booking endpoint: check that a slot isn’t already booked, book it, return a 409 if it’s taken. Claude wrote exactly that. The code checks for existence, checks the reservation flag, and creates the reservation. It’s honestly good code.
Manual testing confirms it works. Booking a reserved slot returns 409. Booking a missing slot returns a not-found error. Booking a free slot succeeds. Every test passes. And that is the trap β every one of those checks confirmed the code is correct in isolation, which was never the question. The thing that actually breaks went untested.
The Check-Then-Act Race Condition
So I wrote a short script that fires two requests at the same slot simultaneously using Promise.all. It grabs slot 5, wipes any existing reservation so the slot starts empty, then fires both requests at once and logs what each one returned:
// concurrency-test.ts
import "dotenv/config";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const SLOT_ID = 5;
const URL = "http://localhost:3000/booking";
async function main() {
// Guarantee the slot starts empty
await prisma.reservation.deleteMany({ where: { slotId: SLOT_ID } });
await prisma.slot.update({
where: { id: SLOT_ID },
data: { isReserved: false },
});
// Fire two requests at the same slot at the same instant
const [a, b] = await Promise.all([
fetch(URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slotId: SLOT_ID }),
}),
fetch(URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slotId: SLOT_ID }),
}),
]);
console.log("Request A:", a.status);
console.log("Request B:", b.status);
}
main().finally(() => prisma.$disconnect());
With the server running, I fire it off with npx tsx concurrency-test.ts. Both requests return 201. The slot gets double-booked.
Here is why. Both requests hit the SELECT first, and both see is_reserved as false, because neither UPDATE has run yet. Both pass the check, both run the update. The check and the act are two separate trips to the database, and in the gap between them, the world changed. Telling the agent to “check first” in plain English didn’t help β that check-then-act pattern is the bug under concurrency. Every line is defensible. The failure only exists between the lines, and that space appears only when two requests interleave.
Naming the Constraint the Agent Couldn’t See
The fix isn’t “prompt better” in a vague sense. On the second pass, I told Claude Code precisely what it missed: this is a check-then-act race condition, two concurrent requests can both pass the check, make the booking atomic under concurrency. Claude replaced the check-then-act with a single conditional update using Prisma’s updateMany, reserving the slot only if it’s still false. The database guarantees just one request matches. If the count is zero, it returns 409. Only the winner creates the reservation.
The lesson is the sequence, not the syntax. Claude got it right once I supplied the invisible requirement. The agent sees your file; it does not see your traffic.
Key Takeaways
- Claude Code writes code that is correct in isolation, because the file is all it can reason about β it cannot see that two requests will run against the same database row at once.
- A check-then-act pattern (check availability, then act on it) double-books under concurrency, because both requests pass the check before either one writes.
- Passing every manual test proves nothing about concurrency; a single click-and-wait test happens back-to-back, while real traffic interleaves.
- Before trusting the green check mark, ask whether the feature touches shared state two requests could hit at once β booking, inventory, balances, rate limits, claiming a username.
- Keep a mental list of concurrency-sensitive verbs β book, claim, reserve, redeem, decrement, transfer β and write a two-request test before you read the agent’s code.
Conclusion
Agentic coding surprises you in production because the demo speaks the language of your file while production speaks the language of your traffic. The skill isn’t trusting Claude Code less. It’s knowing which features run against shared state, testing those with two simultaneous requests, and handing the agent the constraint it couldn’t infer. Do that, and the green check mark stops lying to you.