Back to Technical Mode
Race Condition / TOCTOU (Coupon Double-Redemption)
A Time-Of-Check to Time-Of-Use gap — reading a coupon's used status, then writing it as used in a separate step — lets two nearly-simultaneous requests both pass the check before either commits its write, redeeming a single-use coupon multiple times.
The bug isn't in either individual line of code — it's in the gap of time between the check and the write, where another request can slip through
Firing the same request 20-50 times in parallel (easy with any load-testing or scripting tool) is enough to reliably win this race
The fix is making the check-and-mark a single atomic database operation, not two separate application-level steps
This exact pattern also causes double-spending gift card balances, over-redeeming referral credits, and duplicate order fulfillment from a retried webhook
Want this flow explained in detail as it plays?
POST/api/coupons/redeem
Headers
Host:app.flowground.dev
Content-Type:application/json
Body
Loading...
Quick-Insert Payloads
Defense in depth
- Perform the check-and-mark as one atomic database statement (UPDATE coupons SET is_used = true WHERE code = ? AND is_used = false), and only proceed if that update actually affected a row
- Use a unique constraint or a distributed lock keyed on the coupon code for the duration of the redemption, rejecting concurrent attempts outright
- Treat the coupon table itself as the single source of truth for concurrency control — don't rely on an application-layer in-memory flag or cache that can drift between server instances
Vulnerable vs. Fixed
app.post("/api/coupons/redeem", async (req, res) => {
const coupon = await db.coupons.findOne({ code: req.body.code });
if (coupon.isUsed) return res.status(400).json({ error: "Already redeemed" });
await applyDiscount(req.body.orderId, coupon);
await db.coupons.update({ code: coupon.code }, { isUsed: true }); // written AFTER the check, with a gap in between
res.json({ success: true });
});