A gift card code, a one-time promo, a limited coupon, all typically get built with a check that seems obviously correct: look up the code, confirm it hasn't been used yet, mark it as used, apply the benefit. Tested normally, one request at a time, this works exactly as expected. Fire two redemption requests for the same code at almost the exact same instant, and a surprising number of implementations let both succeed.
Why this happens even in code that looks completely correct
The sequence "check if used, then mark as used" involves two separate steps, and if two requests for the same code arrive close enough together, both can complete the first step, checking that the code is still unused, before either one finishes the second step, marking it as used. Both checks see an unused code, because neither request's "mark it used" update has happened yet at the moment the other one checks. Both then proceed to apply the benefit. The code gets redeemed twice, or the gift card balance gets applied twice, even though the code logically only allowed one use, and even though each individual request, viewed in isolation, followed the rules exactly.
Why this is genuinely easy to miss
This bug is invisible in almost all normal testing, because it requires two requests arriving within a very tight window, something that essentially never happens by accident during ordinary manual testing, one click at a time. It only reliably shows up either through a deliberate attempt to exploit it, firing multiple requests programmatically at once on purpose, or at real scale, where enough legitimate traffic occasionally creates the same timing collision by pure chance.
How this actually gets exploited
Someone who understands this pattern can write a small script that fires several redemption requests for the same code simultaneously, rather than one at a time, specifically to create that timing window. If the backend doesn't guard against it, several of those requests can succeed, each one having checked an as-yet-unmarked code and proceeded, multiplying whatever the code was worth by however many requests won that race.
What actually prevents this
- Use your database's own atomic operations for check-and-update logic, a single operation that checks the condition and updates the record together, rather than two separate steps with a gap between them where another request can slip through.
- Add a unique constraint at the database level on redemption records, so a genuine attempt at double-redemption gets rejected by the database itself as a matter of structural integrity, not just application logic that can be raced.
- For especially high-value redemptions, consider an explicit locking mechanism that ensures only one request can process a given code at a time, even under concurrent load.
How to actually test for this on your own app
Write a small script, or use a basic load-testing tool, to fire several redemption requests for the same test code at the same moment, rather than one after another. If more than one succeeds, that's the race condition, sitting exactly where it would in a real, coordinated attempt to exploit it.
This is a subtler category than most of what I write about, since it requires understanding timing rather than a simple missing check. It's also a real, well-documented pattern, and it's worth testing specifically on anything that involves a limited, one-time-use resource with real value attached.