Learn

What are race conditions in web apps?

Race conditions let parallel requests break rules like single use or balance limits. Learn the impact, why scanners miss them, and how to prevent them.

Updated 2026-10-15

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.

Questions

Are race conditions only a problem in low-level code?

No. Web apps run many requests at once on many servers. Any check-then-act sequence on shared data can race.

What is a TOCTOU bug?

Time of check to time of use. The app checks a condition, then acts on it later, and the condition changes in between. CWE tracks it as CWE-367.

Does adding a mutex in the app server fix it?

Only if there is one server process. With several instances, you need locking or constraints in the shared database or store.

See it on your app.