Have you ever thought about what could happen if two people tried to withdraw from the same bank account at the exact same time?

That’s a classic case of a race condition — and if not handled properly, it can lead to real money vanishing (or duplicating!) out of thin air.

In this post, I’ll walk you through:

  1. What a race condition is (in the context of banking)
  2. How to simulate one in Node.js
  3. How to fix it using a mutex (lock).

What Is a Race Condition?

A race condition occurs when two or more operations access shared data at the same time, and the final result depends on the order in which they execute.

In banking terms:
Two people transfer money from the same account at the same time. If not handled properly, both could think the money is still there — and withdraw it — causing the account to go negative or become inconsistent.

Simulating the Problem in Node.js

Let’s say we have a shared in-memory balance of $100, and two transfer requests come in at the same time:

const express = require('express');
const app = express();
const port = 3000;
let accountBalance = 100; 

function transfer(amount) {
    const balance = accountBalance;
    if (balance < amount) {
        throw new Error('Insufficient funds');
    }
    // Simulate some delay in processing. It can be many reason
    setTimeout(() => {
        accountBalance = balance - amount;
        console.log(`Balance after transfer: $${accountBalance}`);
    }, 1000);
}

app.post('/transfer', (req, res) => {
    const amount = 50;
    try {
        transfer(amount);
        res.send('Transfer completed');
    } catch (error) {
        res.status(400).send(error.message);
    }
});

app.listen(port, () => {
    console.log(`Server running on http://localhost:${port}`);
});

From your terminal -

curl -X POST http://localhost:3000/transfer & curl -X POST http://localhost:3000/transfer &

Did you notice? You still have 50$!
This is broken! If two requests run simultaneously, both read $100, both think there's enough, and both deduct $50 — ending up with a final balance of $50, when it should be $0.

Let's fix the problem -

const { Mutex } = require('async-mutex');
const mutex = new Mutex();

async function transfer(amount) {
    const release = await mutex.acquire();
    try {
        const currentBalance = accountBalance;
        if (currentBalance < amount) {
            throw new Error('Insufficient funds');
        }
        await new Promise(resolve => setTimeout(resolve, 100));
        accountBalance = currentBalance - amount;
        console.log(`Transfer successful. New balance: $${accountBalance}`);
    } finally {
        release();
    }
}

Now, only one transfer runs at a time. The second request waits until the first finishes. And you may have noticed - your balance is now zero.

This is a hypothetical example, but race conditions like this do happen in real-world applications — such as counting page views, managing product stock in e-commerce platforms, and more. The scary part? They often go unnoticed until they cause serious issues.