This article explains race conditions through a practical lab where attackers redeem a single-use coupon twice by exploiting timing gaps in non-atomic code. It demonstrates how check-then-act patterns with separate read and write steps create windows where concurrent requests can slip through validation, and teaches defensive techniques to close those gaps. Race conditions in redemption and payment flows are a real, exploitable vulnerability class found regularly in bug-bounty reports.
- Check-then-act vulnerability patternThe lab demonstrates that DealStore's coupon redemption runs as three separate steps: read whether the coupon is used, apply the $10 discount if not, then mark it used. Because these are separate database operations, two simultaneous requests can both read 'unused' before either writes the 'used' flag, causing both to apply the discount.
- Concurrency exploits timing windowsThe attack works by preparing two identical redemption requests for the same order and firing them as close to simultaneously as possible. Both requests pass the 'is it unused?' check before either has written the used flag, allowing the discount to stack beyond the intended $10 limit. The vulnerability only manifests under concurrent timing—sequential requests never trigger it.
- Atomicity is the core defenseThe article emphasizes that validity checks and the updates enforcing them must happen as a single atomic operation. Three concrete defenses are offered: use a unique database constraint to reject duplicate writes, perform check and update in one atomic statement within a properly-locked transaction, or employ idempotency keys to collapse concurrent or repeated requests into a single effect.
- Real-world scope beyond couponsRace conditions in check-then-act flows enable tangible abuse across payment systems: double-spending gift cards, withdrawing balances multiple times, and claiming limited offers repeatedly. These concurrency bugs appear regularly in bug-bounty reports, typically classified under Business Logic or Broken Access Control vulnerabilities.
- Testing technique for vulnerability discoveryThe article recommends finding races in any application by identifying 'only once' actions—such as redeem, withdraw, vote, or claim—then firing several identical requests simultaneously. If more than one succeeds, the check and write operations are not atomic and the system is vulnerable.
- Why sequential testing misses the bugIf clicking the redeem button sequentially only ever applies the coupon once, developers may falsely conclude the logic is correct. True concurrency exposure only emerges when requests arrive at overlapping moments, making the vulnerability invisible during normal sequential testing.
Bottom line: Race conditions in check-then-act logic are exploitable, real-world vulnerabilities that allow attackers to bypass 'only once' constraints by timing concurrent requests to slip through validation gaps; defend by making the check and update atomic through database constraints, transactions with proper locking, or idempotency keys rather than relying on separate read-then-write operations.
📋 In this walkthrough
What this lab teaches
A race condition happens when the result depends on the timing of events that were assumed to happen one at a time. The classic shape is check-then-act: check a condition, then act on it, with a gap in between where another request can slip in.
The lesson: a validity check and the update that enforces it must happen as one atomic operation. If they are two steps, concurrency can drive a truck through the gap.
How DealStore redeems a coupon
Redeeming WELCOME10 runs as: read whether the coupon is used, apply the $10 discount if not, then mark it used. Each step is a separate database touch, so two requests that arrive together can both pass the 'is it used?' check before either has written the 'used' flag.
The attack approach
- Understand the redemption is check-then-act, with a window between the check and the write.
- Prepare two (or more) identical redemption requests for the same order.
- Send them as close to simultaneously as possible so both pass the 'unused' check.
- Watch the discount stack beyond $10 as both requests apply it.
Full walkthrough (spoiler)
If sequential clicks only ever apply the coupon once, you need true concurrency — open the steps.
Show the step-by-step solution
- Set up two identical 'apply WELCOME10' requests against the same order.
- Fire them simultaneously (the lab provides a way to send concurrent requests) so both execute the check before either writes the used flag.
- Both requests read 'coupon not used', both apply the $10 discount, and only then does the used flag get written.
- The order's discount now exceeds $10. The lab detects the double discount and marks you solved.
Why it works
The code assumed redemptions happen one after another, so the gap between checking 'unused' and writing 'used' felt harmless. Under concurrency that gap is a window: multiple requests all see the stale 'unused' state and all act on it. Nothing is broken in any single request — the bug only appears when they overlap.
Where this happens in the real world
Check-then-act races drive real abuse: double-spending gift cards and coupons, withdrawing a balance more than once, and redeeming limited offers repeatedly. Concurrency bugs in payment and redemption flows are a steady source of bug-bounty reports, and they fall under Business Logic / Broken Access Control depending on what they bypass.
How to defend against it
- Use a unique constraint (one redemption row per coupon per user) so the database rejects the second write.
- Perform the check and the update in one atomic statement or inside a transaction with the right locking.
- Use idempotency keys so repeated or concurrent requests collapse into a single effect.
- Avoid read-then-write-later patterns for anything that must happen only once.
Try it, then go further
Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.
