Concurrent Requests Break Business Rules That Look Airtight

Key takeaway: A rule enforced by reading state and then writing it is not enforced at all under concurrency. The gap between the two operations is where the attacker operates.
The Pattern That Fails
Code reads a balance, confirms it is sufficient, then deducts. Or reads a coupon’s redemption count, confirms it is unused, then marks it used. Or counts existing invitations, confirms the limit is not reached, then inserts.
Every one of these is correct when requests arrive one at a time. Send fifty simultaneously and all fifty read the original state, all fifty pass the check, and all fifty proceed. The limit was enforced against a value that was already stale when it was read.
The exploitation requires no sophistication — a script sending parallel requests, or a browser tool replaying one request many times at once.
Where It Shows Up Financially
The consequences cluster in the places with direct monetary value.
- Redeeming a single-use voucher many times
- Withdrawing more than an account holds
- Applying a referral bonus repeatedly
- Exceeding a rate limit or quota
- Claiming a limited-quantity item more times than stock allows
- Escalating a two-step approval by racing both steps
Loyalty and rewards systems are hit disproportionately because they combine real value with logic that was rarely designed for concurrency and is often implemented outside the core transactional system.
Fixes That Actually Hold
| Technique | Effect |
|---|---|
| Application-level check then write | No protection |
| Database unique constraint | Strong for uniqueness rules |
SELECT ... FOR UPDATE |
Serialises access to the row |
| Atomic conditional update | Strong, single statement |
| Optimistic locking with version column | Strong, needs retry handling |
| Serializable isolation | Strong, costs throughput |
The atomic conditional update is the cleanest option where it applies. A single statement that decrements a balance only if the balance is sufficient, checking rows affected, cannot be raced because the condition and the write are one operation the database evaluates under its own locking.
Unique constraints are the most underused. A rule like “one redemption per user per voucher” is a uniqueness property, and the database enforces uniqueness correctly under any concurrency. Enforcing it in application code instead is choosing a weaker mechanism for something the database already does perfectly.
Row locking with FOR UPDATE works and requires care about lock ordering to avoid deadlock, plus discipline about transaction duration.
Testing For It
Sequential tests never find these bugs, which is why they reach production despite good coverage.
Write tests that fire the same operation concurrently — twenty parallel calls redeeming one voucher — and assert that exactly one succeeds. These tests are straightforward to write and reliably catch the entire class.
Include the cross-endpoint cases too. Some races involve two different operations on the same state, and testing each endpoint alone will not reveal them.
The Bottom Line
Move every invariant into the database as a constraint or an atomic conditional update rather than a read followed by a write. Add concurrent tests asserting that exactly one of N simultaneous operations succeeds, since that is the only kind of test that finds this class of bug.



