A race condition happens when the result of an operation depends on the timing of several requests that touch the same data. The app checks a rule, such as "this coupon has not been used", and then acts on it, but another request slips in between the check and the action. CWE describes this as concurrent execution with improper synchronization (CWE-362) and time-of-check time-of-use (CWE-367).
Updated October 2026
Why it matters
Many business rules are limits: use once, spend no more than the balance, one free trial per account, one vote per user. Race conditions break those limits. The result can be money lost, credits created from nothing, or a user holding a permission that should have been revoked.
These bugs are hard to spot in logs because each request looks normal on its own. The damage only shows when you add them up.
Race conditions also get worse as an app grows. More servers, more workers and faster databases all make it easier for two requests to overlap. A check that was safe on a single small server can quietly become unsafe after a scaling change, with no code change at all. Review these paths again whenever your architecture shifts.
A simple illustration
Acme Corp gives each workspace a one-time setup credit. A member can redeem it from the billing page. The server checks whether the credit is already used, then adds it to the balance and marks it used. If two redemptions arrive at the same moment, both can pass the check before either marks the credit as used.
Vulnerable check:
def redeem_credit(org):
if org.setup_credit_used:
raise Conflict()
org.balance += SETUP_CREDIT
org.setup_credit_used = True
org.save()
Corrected check:
def redeem_credit(org):
updated = Org.objects.filter(
id=org.id, setup_credit_used=False
).update(
setup_credit_used=True,
balance=F("balance") + SETUP_CREDIT,
)
if updated == 0:
raise Conflict()
The corrected version does the check and the change in one atomic database statement. Only one request can flip the flag, so only one gets the credit.
Why automated scanners often miss it
Scanners usually send one request at a time and judge each response alone. A race needs concurrent requests, a known shared resource, and a way to tell that the end state is wrong (two credits instead of one). That last part needs business context. Timing also varies between runs, so a race may show up once in fifty attempts, which looks like noise to tools that expect stable results.
How to prevent it
- Combine check and update into one atomic operation (conditional update, compare and set).
- Use database constraints, such as unique indexes, to make duplicates impossible.
- Use row-level locks or serializable transactions for multi-step updates.
- Use idempotency keys on payment, redemption and other one-time actions.
- Keep locks in the shared store, not in one app process.
- Revoke sessions and permissions in a way that takes effect immediately everywhere.
How to test your own app for it
Only test software you own or have written permission to test.
- List every action that enforces a limit or a single use.
- Review the code for each and confirm the check and the change are atomic.
- Confirm a database constraint backs every "only one" rule.
- In a test environment, run the same action concurrently and confirm the end state is correct.
- Add concurrency tests to CI for your most sensitive actions.
How Wakefield helps
Wakefield's whitebox agents read your source on the default branch and on every pull request, trace risky paths, comment inline and offer a patch with a confidence level. A separate verifier replays each finding before it reaches you. See Whitebox testing.