Stop 36 of 42 · The Back Office · Race conditions

Two tellers, one balance, paid twice

Mallory has 100 in her account. She walks up to two teller windows and asks both to withdraw 100 at exactly the same moment. Each teller checks the notebook, sees 100, and pays out. Neither has written “0” yet when the other looks.

Severity HighCategory Business LogicThe Back OfficeSources checked 25 Oct 2026

Watch it happen

Play it as

Two tellers read the same balance of 100 at once, both approve a 100 withdrawal, and the bank pays out 200.

Arrival

Mallory walks in. She has 100 in her account.

Mallory says: I’d like to withdraw.
1 / 5

In bank terms and app terms

In the bankIn your app
Two tellers at onceTwo simultaneous requests to the same endpoint
Reading the notebook then writingRead balance, check it, then update (check-then-act)
Notebook lockRow lock, transaction or atomic UPDATE … WHERE balance >= amount
Only pay onceIdempotency keys and unique constraints for coupons and refunds

Are you exposed?

You might be exposed if…
  • Your code reads a value, checks it in JavaScript, then writes it back in a separate query.
  • You have one-time actions (coupons, gift cards, votes, refunds) that only check “has this been used?” before acting.
  • You have never tested what happens when the same request is sent 10 times in parallel.
What to do about it
  1. Do the check and the change in one atomic statement: UPDATE … SET balance = balance - 50 WHERE balance >= 50.
  2. Wrap multi-step changes in a transaction that also locks the row (SELECT … FOR UPDATE) or uses SERIALIZABLE isolation with retries. A plain transaction alone is not enough.
  3. Add unique constraints and idempotency keys so a repeated coupon, order or refund can only happen once.

Track these in your to-do list →

It has happened to real companies

Real-world breaches

Starbucks (2015) — Security researcher Egor Homakov found that by using two sessions of the same account at once, he could make Starbucks' gift card transfer process the same $5 balance twice, ending up with $20 across two cards.

What it looks like in code

// BAD: Check-then-act without locking
export async function POST(request: Request) {
  const { amount } = await request.json()

  // Read current balance
  const credits = await db.query("credits", `user_id = '${userId}'`)
  const balance = credits[0].balance

  // Check
  if (balance < amount) return error("Insufficient")

  // GAP: Another request can read the same balance here

  // Act
  await db.update("credits", { balance: balance - amount })
  return success({ redeemed: amount })
}

Ask your AI tool to fix it

Paste this into Cursor, Claude, Lovable, Bolt or whatever you build with.

Prompt
Find every endpoint that changes money, credits, stock, coupons, votes or refunds. Replace read-check-write sequences with a single atomic UPDATE with a WHERE condition, or a database transaction with row locking. Add unique constraints or idempotency keys for one-time actions. Then write a test that sends 10 parallel identical requests and asserts only the allowed number succeed.

Where this comes from

The facts, figures and fixes on this page were checked against these sources on 25 Oct 2026. They are linked so you can check for yourself.

Spot a mistake or something out of date? Report a correction · How these guides are made and checked

Go deeper in isitsecure

The free, open-source scanner checks for most of the problems in these guides.