The window

Most race conditions are not exotic. They are an ordinary sequence that assumed it was alone.

1. read   balance = 100
2. check  balance >= 100   -> ok
3. write  balance = balance - 100

Between step 2 and step 3 the server is doing other things. If a second request runs steps 1 and 2 before the first request reaches step 3, both requests see a balance of 100, both pass the check, and both deduct. The account goes to -100, or a one-use coupon gets used twice, or a gift card funds two withdrawals.

That gap is the check-then-act window. It is the same shape whether the state lives in a database row, a cache entry, a file on disk, or an in-memory counter behind a lock that is too narrow.

Why re-sending the request does not work

The obvious test is to fire the request twice, quickly. It usually fails to reproduce, and the reason is physical: between two sequential requests there is network latency, TLS, connection reuse, application scheduling and a database round trip. The window is often narrower than any of those. Firing a thousand requests in a loop is mostly a test of the loop.

What you need is for the requests to arrive at the application at the same time, not one after another.

Single-packet attacks

HTTP/1.1 is a text protocol over a stream, so a client can place several complete requests into one TCP segment. The kernel delivers that segment to the server once. The server then reads request one, request two and request three from the same buffer, back to back, with no network gap between them.

That collapses the arrival skew from milliseconds to microseconds — often below the check-then-act window. Hugin's RatRace engine builds these batches directly: it opens the connection, writes the request set into a single packet, and reads the responses back individually.

Two refinements matter in practice:

  • Last-byte synchronisation. Send all but the final byte of each request, then release the final bytes together. The server has everything except the byte that terminates each message, so processing starts the instant the packet lands.
  • Barrier coordination. For protocols that need more setup, gate a batch of workers behind a shared barrier so they act on the same signal rather than on their own timers.

Proving it, not guessing it

A timing difference is not a finding. Two responses arriving 3 ms apart is noise. A finding is a result the application logic forbids:

  • A coupon with max_uses = 1 is accepted twice and both orders are fulfilled.
  • A balance that should never fall below zero goes negative.
  • A single-use referral code is credited to two accounts.

So the test is: perform the race, then read the state back through a normal request and show the invariant is broken. If the state is unchanged, there is no bug — regardless of how the timing looked.

Where to look first

Race conditions cluster where a rare, valuable action is guarded by a cheap check:

  • Coupon and voucher redemption, and gift-card balance transfers.
  • Referral and sign-up bonuses with a one-per-account rule.
  • Inventory reservation, ticket booking and limited-stock checkout.
  • Account recovery: consuming a reset token, or unlinking a second factor.
  • File operations that check existence before writing (the classic TOCTOU).

Each of these has the same tell — a rule that says "only once" enforced by a read the attacker's own parallel requests can also read.