What is Race Condition?
Your app checks a user's credit balance, confirms they have enough, then deducts the amount. Sounds fine — until someone sends 10 identical requests at the exact same time. All 10 read the same balance. All 10 pass the check. All 10 deduct. The user just spent credits they didn't have.
This is a race condition. The gap between reading a value and writing it back creates a window where concurrent requests see stale data. Databases call this a TOCTOU bug — Time-of-Check to Time-of-Use. It's the same class of issue that lets people redeem a coupon code 50 times or claim a referral bonus for accounts that don't exist yet.
Race conditions are hard to catch in development because they require concurrency to trigger. Your test suite runs one request at a time. Your local dev server handles one request at a time. But production? Production handles hundreds simultaneously — and attackers know exactly how to exploit that.
How does Race Condition work?
A race condition needs a non-atomic check-then-act pattern: the app reads some state, makes a decision based on it, then writes new state — with no lock preventing other requests from reading the same stale value in between.
Here's a typical credit deduction endpoint:
// app/api/credits/spend/route.ts
export async function POST(req) {
const { userId, amount } = await req.json();
// Step 1: Check balance
const user = await db.query(
'SELECT balance FROM accounts WHERE id = $1',
[userId]
);
if (user.rows[0].balance < amount) {
return Response.json({ error: 'Insufficient balance' }, { status: 400 });
}
// Step 2: Deduct (but 10 concurrent requests all passed Step 1)
await db.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, userId]
);
return Response.json({ success: true });
}// app/api/credits/spend/route.ts
export async function POST(req) {
const { userId, amount } = await req.json();
const result = await db.transaction(async (tx) => {
// SELECT FOR UPDATE locks the row until commit
const user = await tx.query(
'SELECT balance FROM accounts WHERE id = $1 FOR UPDATE',
[userId]
);
if (user.rows[0].balance < amount) {
throw new Error('Insufficient balance');
}
await tx.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, userId]
);
return { newBalance: user.rows[0].balance - amount };
});
return Response.json({ success: true, balance: result.newBalance });
}Why do AI tools generate Race Condition vulnerabilities?
AI code generators write sequential logic. They produce code that works perfectly when requests arrive one at a time — which is exactly how the generated app gets tested. Concurrency safety is almost never part of the output.
- Sequential thinking by default. When you ask for a "spend credits" endpoint, the model writes read-check-write in three steps. It doesn't consider that two requests might be in the middle of those steps simultaneously.
- Transactions are rarely generated unprompted. Database transactions and row-level locks require explicit intent. AI generates the happy path — and the happy path doesn't need FOR UPDATE.
- Testing never reveals the bug. Single-threaded test suites, local dev servers, and manual testing all process one request at a time. The race condition only surfaces under real concurrent load.
Race conditions are one of the hardest bugs to find through code review alone. The code looks correct line by line — the problem only exists in the timing between lines, under concurrency.
Common Race Condition patterns
Double-spend on credits or tokens
Send 10 simultaneous requests to spend the same balance. All 10 read the old value, all 10 succeed.
Coupon or promo code reuse
A one-time coupon gets checked and marked as used — but 5 parallel requests all check before any marks it.
Account registration duplicates
Two simultaneous signups with the same email both pass the "email not taken" check.
Inventory overselling
Limited stock item shows 1 remaining. Three concurrent purchases all see 1 and complete.
How Flowpatrol detects Race Condition
Flowpatrol tests for race conditions by firing concurrent requests at state-changing endpoints:
- 1Identifies state-changing endpoints by mapping routes that modify balances, counters, tokens, or one-time resources.
- 2Fires parallel requests — sends 10-20 identical requests simultaneously to trigger TOCTOU windows.
- 3Compares expected vs. actual state — checks whether the balance dropped more than it should, or a coupon was applied multiple times.
- 4Reports the timing window with request timestamps, response data, and the exact state inconsistency found.
This is a test most teams never run. Flowpatrol automates exactly the kind of concurrent abuse that real attackers attempt on launch day.
Related terms
Check your app for Race Conditions.
Flowpatrol fires concurrent requests at your state-changing endpoints. Find double-spend bugs before your users do.
Try it free