Web Security

Race Conditions in Web Applications: Detection, Exploitation, and Prevention

A technical guide to race condition vulnerabilities in web applications: TOCTOU, concurrent request handling, race windows in API endpoints, ticketmaster-style racing, and how to test for them.

Advanced13 min·Nyxeara Security Research·2026-09-21·CWE-362
race-conditionconcurrencyweb-securityapi-securitytoctouadvanced-pentesting
Prerequisites
what-is-httpapi-security-testingauthentication-vulnerabilities

Short answer

Race conditions in web applications happen when a server handles two overlapping requests for the same resource and the outcome depends on which finishes first. The most dangerous pattern is "claim-then-verify" — the server lets a request through before checking whether the action is still valid.

The idea in one minute

Imagine a store selling one limited-edition item. Four customers hand the cashier their credit cards at exactly the same moment. The cashier runs all four cards before the inventory system decrements the stock to zero. All four transactions go through, but there was only one item.

Every server processes requests concurrently. Two requests that arrive within milliseconds of each other can both read the initial state ("in stock: 1"), both pass the validation check ("stock > 0, approve"), and both execute the mutation. This is a race condition. The window between the "read" and the "write" is the race window.

Why race conditions exist

The root cause is always the same: the server reads a value, makes a decision based on that value, and then writes the new value — but between the read and the write, another request can read the same original value. This three-step sequence is a "time-of-check to time-of-use" (TOCTOU) vulnerability.

Request A: READ stock → 1 → DECISION: OK → WRITE stock → 0
Request B:                      READ stock → 1 → DECISION: OK → WRITE stock → 0

Both requests read stock = 1 because neither write has happened yet. Both approve. Both write. Result: two orders for one item.

Where to find race conditions

Coupon codes and discounts

The classic race condition. Send 20 simultaneous requests with the same coupon code. If more than one succeeds, the coupon was applied multiple times — a single-use coupon effectively became unlimited.

for i in {1..20}; do
  curl -s -X POST https://target.com/api/checkout \
    -H "Cookie: session=..." \
    -d '{"coupon":"SAVE50"}' &
done
wait

Balance transfers and withdrawals

Banking applications, in-app currency transfers, and loyalty point systems are common targets. Send multiple withdrawal requests simultaneously:

for i in {1..10}; do
  curl -s -X POST https://target.com/api/wallet/withdraw \
    -H "Authorization: Bearer token" \
    -d '{"amount": 100, "destination": "attacker"}' &
done
wait

If the balance is 100 and two withdrawals of 100 succeed, the race was exploitable.

Shopping cart and inventory

Limited-quantity items, ticket sales, and reservation systems. The difference between a successful exploit and a theoretical finding is whether the item was actually overbooked. Many systems detect race conditions at the database level but silently cancel the extra orders — the race still happened, but the user sees only one success.

Email and SMS verification bypass

Registration flows often verify email or phone ownership. The verification code is sent, the user submits it, and the account is marked verified. If the verified check and the account activation are in different requests, send both simultaneously:

Request A: READ verification_status → "unverified" → DECISION: send verification code
Request B:                      READ verification_status → "unverified" → DECISION: send verification code

Two verification codes for one account. Or worse, the account activation endpoint:

Request A: SUBMIT code "123456" → CHECK code → CORRECT → ACTIVATE account
Request B: SUBMIT code any →                          → ACTIVATE account

If the activation check is a SELECT and the activation is an UPDATE, and both request A and B pass the SELECT before either executes the UPDATE, request B activates without supplying the correct code.

File upload race conditions

An upload endpoint validates the file, stores it temporarily, then moves it to the final location. Between the validation and the move, a concurrent request can replace the validated file with a malicious one. This is a real attack vector that bypasses upload validation entirely.

Detection methodology

Race conditions require precision timing. Manual testing with two browser tabs is rarely reliable because the timing window is in milliseconds. Automated tools are essential.

Single-packet attack: Send multiple requests in the same TCP packet so they arrive at the server simultaneously. Most HTTP libraries do not support this natively. Tools like Turbo Intruder (Burp Suite extension) and race-the-web support single-packet or last-byte-synchronized attacks. The key technique: send all requests within the same TCP segment so the server's accept loop processes them before any response is sent.

Last-byte synchronization: Send the HTTP headers for all requests simultaneously, holding back the final byte of each. Release all final bytes at the same moment. The requests arrive at the server within microseconds of each other, narrowing the race window as much as possible.

Verification: real vulnerability or false positive?

A race that succeeds twice in a test environment but only once in production may not be exploitable in production. Database transaction isolation levels and connection pooling affect race windows. To confirm exploitability:

  1. Run the race 100 times. If it succeeds more than once, the race exists.
  2. Check whether the database uses transactions with serializable isolation. Repeatable read or read committed are vulnerable; serializable protects against write-write races but not read-write races.
  3. Confirm that the side effects of the second request actually persisted — a database unique constraint might silently roll back the second insert while returning a success response to the client. Always check the application state after the race, not just the HTTP response codes.

Real-world impact

A major airline had a race condition in their miles-transfer endpoint that allowed users to transfer the same miles to multiple accounts before the balance was decremented. The bug existed for four years and was detected only after an audit of concurrent request patterns. A cryptocurrency exchange had a race condition in their withdrawal system that allowed attackers to withdraw funds multiple times before the balance update propagated to read replicas. The fix: doing balance checks and updates in a single database transaction.

Prevention

All prevention strategies for race conditions boil down to the same principle: make the check and the mutation atomic.

  1. Database transactions with row-level locking. SELECT ... FOR UPDATE locks the row until the transaction commits, preventing concurrent reads from seeing the stale value.
  2. Optimistic locking with version fields. Every update includes WHERE version = ?. If the version changed between the read and the write, the update affects zero rows and the application retries.
  3. Atomic database operations. Instead of READ balance; UPDATE balance = balance - amount, execute UPDATE account SET balance = balance - amount WHERE balance >= amount AND account_id = ?. The database handles the atomicity internally.
  4. Distributed locking for cross-service operations. Use Redis locks or ZooKeeper for operations that span multiple database transactions or microservices.
  5. Idempotency keys. Require a unique idempotency key on state-changing requests. If the same key is received twice, the second request returns the result of the first without re-executing the mutation.

Related vulnerabilities

  • Insecure direct object references — Often combined with race conditions for maximum impact (race a coupon application across multiple accounts).
  • Authentication vulnerabilities — Race conditions in auth flows (verify-before-activate) bypass verification entirely.
  • Business logic flaws — Race conditions are always business logic flaws first, concurrency bugs second.

Testing methodology (do this safely)

Identify all endpoints that read a resource and then write a related resource. Create a list of requests that should be mutually exclusive — applying a single-use coupon twice, withdrawing more than the balance, activating an account without verification. Use a race-condition testing tool to send 10–30 simultaneous requests. Compare the number of successful responses with the expected limit. Verify the application state after each test. Always test on your own applications or authorized bug bounty programs.

Further reading

Nyxeara perspective

Race conditions are one of the most difficult vulnerability classes for automated scanners to detect because they require precise timing and multiple concurrent requests. The Nyxeara platform addresses this by supporting last-byte-synchronized parallel request execution in the Phase 6 workflow engine. When a finding is suspected to have a concurrency component (coupon, withdrawal, inventory), the verification playbook sends a batch of concurrent requests with TCP-level synchronization and checks the application state against the expected invariant. The verification engine reports RACE_CONFIRMED only when the database state shows unambiguous evidence of concurrent mutation — a coupon applied twice, a balance over-drafted, or a verification bypassed.

Published 2026-09-21 · Updated 2026-09-21