Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Web Security (OWASP Top 10:2025)  Module 7 – A06:2025 Insecure Design

Race conditions, TOCTOU and double-spend

Reproduce a double-spend in async JavaScript, then fix check-then-act races with conditional updates, unique constraints, locks and idempotency keys.

  • Advanced
  • 20 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free intro, Pro: 4 of 20 minutes free
Free: A race is a check and an act with a gap between them. With a pass: the other 10 sections and 3 interview answers, about 16 minutes.

What you will learn

  • Explain how check-then-act races happen in async JavaScript and in threaded back ends
  • Reproduce a race on purpose in a test by controlling the order of the steps
  • Fix races with conditional updates, unique constraints, transactions with locks or idempotency keys
  • Choose the control that fits limit overruns, double spends, retried requests and one-time tokens
  • Monitor production for duplicate and parallel attempts with invariant checks and alerts

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how check-then-act races happen in async JavaScript and in threaded back ends
  • Reproduce a race on purpose in a test by controlling the order of the steps
  • Fix races with conditional updates, unique constraints, transactions with locks or idempotency keys
  • Choose the control that fits limit overruns, double spends, retried requests and one-time tokens
  • Monitor production for duplicate and parallel attempts with invariant checks and alerts

Before you start

On this page

A wallet holds 100 credits, and two requests to spend 80 arrive a few milliseconds apart. Each one checks the balance, sees 100 and goes ahead, so the wallet pays out 160 credits and ends at −60. Race conditions like this one let the order in which two operations happen decide the result, and some orders break a rule that each request kept on its own.

You will see why a single-threaded JavaScript server races too, reproduce a race on purpose in a test, and fix each kind of race with the control that fits it.

A race is a check and an act with a gap between them

MITRE’s list of weaknesses gives the pattern a name: CWE-367, time-of-check to time-of-use, or TOCTOU: Time-of-check to time-of-use (CWE-367): code checks the state of something, then uses it, and the state changes in between, so the check no longer holds when the code acts on it.. The code checks the state of something, then uses it, and the state changes in between. TOCTOU is a child of CWE-362, the general race condition: A weakness in which the result depends on the order in which two operations run, and some orders break a rule, such as two withdrawals that both pass the balance check before either subtracts (CWE-362)., in which two operations share a resource without proper synchronisation. A double-spend: The same balance, coupon or ticket used twice because two requests passed the same check before either recorded its use. It is the money form of a race condition. is the money version: the same balance, coupon or ticket spent twice because two requests passed the same check.

JavaScript runs one piece of code at a time, which makes race conditions look impossible in Node.js. They are not: every await hands control back to the event loop, which runs other requests while this one waits for its database. The program below starts two withdrawals with Promise.all, first with an await between the check and the act, then with one call that checks and changes together:

Two withdrawals at once, weak and fixed JavaScript · wallet/wallet_race.mjs
// Two withdrawals of 80 credits from a balance of 100, started at the same time with Promise.all.
// The store is in memory, but every call is async, as a database call is, so each await lets the other request run.
function makeStore(balance) {
  const log = [];
  return {
    log,
    async read(who) {
      log.push(`${who} reads ${balance}`);
      return balance;
    },
    async recordPayout(who, amount) {
      log.push(`${who} records a payout of ${amount}`);
    },
    async subtract(who, amount) {
      balance -= amount;
      log.push(`${who} subtracts ${amount}: balance ${balance}`);
    },
    // Checks and changes in one step, with no await in between: what UPDATE ... WHERE balance >= ? does.
    async subtractIfAtLeast(who, amount) {
      const ok = balance >= amount;
      if (ok) balance -= amount;
      log.push(`${who} ${ok ? `subtracts ${amount}: balance ${balance}` : `is refused: balance ${balance}`}`);
      return ok;
    },
    get balance() {
      return balance;
    },
  };
}

async function weakWithdraw(store, who, amount) {
  const balance = await store.read(who); // check ...
  if (balance < amount) return 'insufficient';
  await store.recordPayout(who, amount); // ... a gap ...
  await store.subtract(who, amount); // ... act
  return 'ok';
}

async function fixedWithdraw(store, who, amount) {
  if (!(await store.subtractIfAtLeast(who, amount))) return 'insufficient';
  await store.recordPayout(who, amount);
  return 'ok';
}

for (const [name, withdraw] of [['weak', weakWithdraw], ['fixed', fixedWithdraw]]) {
  const store = makeStore(100);
  const results = await Promise.all([withdraw(store, 'A', 80), withdraw(store, 'B', 80)]);
  console.log(`${name}: ${results.join(', ')}; balance ${store.balance}`);
  for (const line of store.log) console.log(`  ${line}`);
}

Output

weak: ok, ok; balance -60
  A reads 100
  B reads 100
  A records a payout of 80
  B records a payout of 80
  A subtracts 80: balance 20
  B subtracts 80: balance -60
fixed: ok, insufficient; balance 20
  A subtracts 80: balance 20
  B is refused: balance 20
  A records a payout of 80

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node wallet_race.mjs

The weak run paid both withdrawals and ended at −60, a double-spend of 80 credits; the fixed run paid one and ended at 20. Step through the weak run one message at a time, then the fix:

Interactive · message by message Two withdrawals of 80 from a balance of 100 Step through the weak run, where both requests check before either acts, then the fixed run, where checking and changing are one step.
Two withdrawals of 80 from a balance of 100, step 7 of 7. Messages so far: Request A to Wallet store: read balance; Request B to Wallet store: read balance; Request A to Wallet store: subtract 80; Request B to Wallet store: subtract 80; Request A to Wallet store: subtract 80 if at least 80; Request B to Wallet store: subtract 80 if at least 80.Request AWallet storebalance 20Request Bread balanceread balancesubtract 80subtract 80subtract 80 if at least 80subtract 80 if at least 80refused

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. A checks. Request A asks for the balance and gets 100, which covers 80. While A waits for its next database call, the server runs other work, and request B is next.
  2. B checks. Request B asks for the balance before A has changed anything, and it also gets 100. Both requests now believe they may spend 80.
  3. Both record a payout. Each request records a payout of 80. These awaits are the gap between the check and the act, and each one lets the other request run.
  4. A subtracts. Request A subtracts 80, and the balance is 20. Its check was true when it ran, and still is for A.
  5. B subtracts. Request B subtracts 80 on the strength of its own old check. The balance is now −60, and the wallet has paid out 160 from 100.
  6. The fix, one step. Start again from 100. Request A asks the store to subtract 80 only if the balance is at least 80, in one step. Nothing can run between that check and that change.
  7. B is refused. Request B makes the same request and the store refuses it, because 20 is less than 80. One withdrawal is paid, and the balance never goes below zero.

Neither request did anything wrong on its own. Each checked the balance it had read, and each read was true when it happened; the rule broke only because the two ran together. So the fix is never a better check, but a check that nothing can come between. Every fix closes the gap, and four controls cover most races:

  • A conditional update: One database statement that checks and changes together, such as UPDATE accounts SET balance = balance - 80 WHERE id = 1 AND balance >= 80. Its row count says whether the change happened. checks and changes in one database statement, and its row count says whether it happened.
  • A unique constraint makes the database refuse a second row for a rule such as once per student.
  • A transaction with a row lock makes the second request wait until the first has finished.
  • An idempotency key: A unique value a client sends with every attempt of one operation, so that the server can recognise a retried request and return the first answer instead of doing the work again. lets the server recognise a retried request and return its first answer.

The fixed run above is a conditional update in miniature: one call that subtracts only while the balance still covers the amount. A unique constraint and a row lock are kept by the database itself, whichever request reaches it first. An idempotency key answers a different duplicate, the same request sent again by a retry or a double tap.

Checkpoint

Check: The gap between a check and an act

  1. Question 1 of 2 A Node.js handler reads a coupon's used flag, awaits a call to the payment provider, then marks the coupon used. Two requests for the same coupon arrive together. What can happen?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Both read "unused" before either marks it, so the coupon is used twice

    Any await, callback or I/O between a check and the act it guards is a gap. In the lesson's run, two withdrawals of 80 both read 100 and the balance ended at −60.

  2. Question 2 of 2 Which name fits a bug in which code checks that a file is safe, and then opens it after another process has replaced it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: TOCTOU, time-of-check to time-of-use

    TOCTOU is the general pattern, a child of the race condition CWE-362; a double-spend is the same pattern applied to money or anything that may be used once.

Flashcards · 4 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are TOCTOU and a double-spend?

    Time-of-check to time-of-use (CWE-367) is code that checks the state of something, then uses it, while the state changes in between; it is a child of the general race condition CWE-362, where two operations share a resource without proper synchronisation. A double-spend is the money version: the same balance, coupon or ticket spent twice because two requests passed the same check.

  2. Why can a single-threaded Node.js server have race conditions?

    Every await hands control back to the event loop, which runs other requests while this one waits for its database. In the lesson two withdrawals of 80 from a wallet of 100 credits, started together, ended at -60, while the fixed run paid one and ended at 20.

  3. Why is the fix for a race never a better check?

    Each request was correct by itself: it checked a balance that was true when it read it, and the rule broke only because the two ran together. The fix is a check that nothing can come between, which closes the gap.

  4. Which four controls close most races?

    A conditional update (checks and changes in one statement, and its row count says whether it happened); a unique constraint (the database refuses a second row for a rule such as once per student); a transaction with a row lock (the second request waits for the first); and an idempotency key (the server recognises a retried request and returns its first answer, which answers the same request sent twice).

Reproducing a race on purpose, every time Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • One statement that checks and changes, in the database
  • Threads and processes race the same way
  • Retries and duplicates need idempotency keys
  • A lock across servers needs a fencing token
  • Choosing the control for each race
  • Watching for races in production
  • Practise: fix three check-then-act races
    • Fix three check-then-act races
  • Interview questions
    • What is a TOCTOU race, and why does a single-threaded Node.js server still have them?
    • A wallet API reads the balance, checks that it covers the payment, calls the payment ledger, then writes the new balance. Support finds a few wallets below zero every week. Diagnose it, fix it and prove the fix.
    • A once-per-student coupon is enforced by a check in application code. After the service grew to four servers, some students redeem it twice. How do you fix it so that it holds for any number of servers?
    • A mobile app retries a payment request when it times out, and some customers are charged twice. Design an idempotency scheme for the payments API: what the client sends, what the server stores and how it answers each kind of duplicate.
  • Key takeaways

1 diagram · 4 runnable examples (JavaScript, Python, SQL) · 1 exercise with sample tests · 3 interview answers · about 39 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Terms in this lesson

conditional update
One database statement that checks and changes together, such as UPDATE accounts SET balance = balance - 80 WHERE id = 1 AND balance >= 80. Its row count says whether the change happened.
double-spend
The same balance, coupon or ticket used twice because two requests passed the same check before either recorded its use. It is the money form of a race condition.
idempotency key
A unique value a client sends with every attempt of one operation, so that the server can recognise a retried request and return the first answer instead of doing the work again.
race condition
A weakness in which the result depends on the order in which two operations run, and some orders break a rule, such as two withdrawals that both pass the balance check before either subtracts (CWE-362).
TOCTOU
Time-of-check to time-of-use (CWE-367): code checks the state of something, then uses it, and the state changes in between, so the check no longer holds when the code acts on it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.