Foundations roadmap

Race Conditions and Shared State

Log in to save this

Saving keeps this in your list across devices. It's a free account — no card.

In the Worker District you meet Echo, who exists in both timelines at once. Two copies of Echo each read the same ledger, each add their coin, and each write the total back, and one coin disappears. That is a race condition: the result depends on the timing of two things that touch the same data. It is one of the most common bugs in real web apps, and it almost never shows up when you test alone.

The lost update

Here is a "buy" endpoint that looks fine:

app.post('/buy/:id', async (req, res) => {
  const product = await db.getProduct(req.params.id); // stock = 1
  if (product.stock < 1) return res.status(409).send('Sold out');
  await db.setStock(product.id, product.stock - 1);    // writes 0
  await db.createOrder(req.user.id, product.id);
  res.send('Bought');
});

Two customers click at the same moment. Both requests read stock = 1, both pass the check, both write 0, and both get an order. You sold one item twice. The same shape loses likes on a counter, credits on a balance and seats on a flight. The pattern is always read, change in your code, write back, with a gap in the middle.

Single-threaded Node still races

"Node runs on one thread, so two requests can't collide" is a trap. Every await is a gap where another request can run. Request A reads stock and pauses at await; request B reads the same stock; then both write. No two lines ever run at the same instant, and the update is still lost.

It gets worse in production. You usually run several copies of your server behind a load balancer, or several serverless instances. They share nothing in memory, so a JavaScript variable used as a flag or lock only protects requests on one copy. The shared data lives in the database, so the fix belongs there too.

Fix 1: let the database do the math

Instead of reading the value into JavaScript, send the change itself:

UPDATE products
SET stock = stock - 1
WHERE id = $1 AND stock > 0;

The database applies this as one atomic step per row, so two of these can never both see stock = 1. The WHERE stock > 0 part is the check. If the row count comes back as 0, the item was sold out and you answer with a 409 instead of creating an order. The same idea works for counters (SET likes = likes + 1) and balances (SET balance = balance - $2 WHERE balance >= $2).

Fix 2: transactions for steps that belong together

Decrementing stock and creating the order should succeed or fail together. A transaction groups them:

BEGIN;
UPDATE products SET stock = stock - 1 WHERE id = $1 AND stock > 0;
INSERT INTO orders (user_id, product_id) VALUES ($2, $1);
COMMIT;  -- or ROLLBACK if the update touched 0 rows

Be careful about what a transaction promises. It makes the group all-or-nothing, but at Postgres's default isolation level it does not stop two transactions from both reading the same old value with a plain SELECT and then writing. Keep the atomic update inside the transaction, or lock the row first (the next mission covers that).

Fix 3: unique constraints for "only one"

A double-clicked signup button sends two requests. Both check "is this email taken?", both see no, and both insert. Checking in code first is another read-then-write race. A unique constraint makes the database refuse the second insert:

ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);

Catch the duplicate-key error (code 23505 in Postgres) and answer "email already registered." Use the same tool for "one vote per user per poll" or "one active cart per user."

How to catch races before users do

Races hide when you click through the app alone. Force the timing instead: send many requests at once and check the result.

await Promise.all(Array.from({ length: 20 }, () => fetch(url, { method: 'POST' })));
// then check: with stock 5, exactly 5 orders and stock 0?

In production, the signs are numbers that don't add up: more orders than stock, a negative balance, two accounts with one email. When you see one, look for a read-modify-write with an await in the middle.

Resources

Curated resources for this node are on the way. Use what you already know how to search for, and check back soon.