DEV Community

Saurav Pandey
Saurav Pandey

Posted on

Demystifying Race Conditions and Deadlocks in Node.js Applications

What Are Race Conditions and Deadlocks?

A race condition occurs when two or more computer operations attempt to access and modify the same shared resource simultaneously, causing the outcome to depend unpredictably on which operation finishes first. A deadlock happens when two or more operations are indefinitely blocked because each operation is holding a resource that the other operation needs before it can proceed. In simple terms, a race condition causes incorrect data due to bad timing, while a deadlock causes a complete system freeze due to mutual waiting.

A Relatable Real-Life Analogy

Imagine a shared checking account containing exactly $100. Two account holders, Alice and Bob, insert their debit cards into separate ATM machines at the exact same second to withdraw $100.

If the banking system experiences a race condition, both ATMs read the balance as $100 simultaneously. ATM A approves Alice's request and updates the balance to $0, while ATM B simultaneously approves Bob's request and also sets the balance to $0. Both walk away with $100, leaving the bank short by $100 because the operations overlapped in time.

Now imagine Alice and Bob cooking a meal in a small kitchen. Alice holds the frying pan and needs the spatula to continue. Bob holds the spatula and needs the frying pan. Alice refuses to give up the pan until she gets the spatula, and Bob refuses to yield the spatula until he gets the pan. Neither can make progress, and dinner is stuck indefinitely. That is a deadlock.

Why It Matters Daily in Tech

In modern software engineering, web servers process thousands of requests concurrently (at the same time). If your application handles high traffic—such as an e-commerce flash sale or a flight reservation platform—unhandled race conditions can cause double-booking, duplicate payments, or inventory going negative.

Deadlocks are equally destructive: when database transactions or system threads lock each other out, API endpoints hang, database connection pools get exhausted, and the entire application stops responding to users. Engineers actively write code to prevent these issues by using database locks, atomic operations, and strict locking hierarchies.

Code Example: Preventing Race Conditions in Express & MySQL

Consider a naive Express.js route updating user points in a MySQL database. If two requests come in at once, reading the balance first before writing can cause a race condition.

Here is the problematic approach versus the safe atomic solution in Node.js:

const express = require('express');
const app = express();
const mysql = require('mysql2/promise');

const pool = mysql.createPool({ host: 'localhost', user: 'root', database: 'app' });

// BAD: Race condition vulnerability!
app.post('/deduct-points-naive', async (req, res) => {
  const { userId, amount } = req.body;

  // Step 1: Read balance
  const [rows] = await pool.query('SELECT points FROM users WHERE id = ?', [userId]);
  const currentPoints = rows[0].points;

  if (currentPoints >= amount) {
    // Artificial delay simulating network latency
    await new Promise(resolve => setTimeout(resolve, 100));

    // Step 2: Write back updated balance (Overwrites parallel updates!)
    await pool.query('UPDATE users SET points = ? WHERE id = ?', [currentPoints - amount, userId]);
    return res.send({ success: true });
  }

  res.status(400).send({ error: 'Insufficient points' });
});

// GOOD: Atomic operation prevents race condition!
app.post('/deduct-points-safe', async (req, res) => {
  const { userId, amount } = req.body;

  // The update happens atomically in a single database step
  const [result] = await pool.query(
    'UPDATE users SET points = points - ? WHERE id = ? AND points >= ?',
    [amount, userId, amount]
  );

  if (result.affectedRows === 0) {
    return res.status(400).send({ error: 'Insufficient points or user not found' });
  }

  res.send({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

By executing the subtraction directly inside the SQL query (UPDATE users SET points = points - ?), MySQL handles the operation atomically, ensuring no two concurrent requests calculate points based on stale data.

The Takeaway

Concurrency brings speed and responsiveness to modern web applications, but without proper controls, it introduces subtle bugs that only surface under heavy traffic. Race conditions corrupt your data when operations run out of order, whereas deadlocks freeze your application when operations wait on each other in circles. Mastering atomic state updates, explicit database transactions, and consistent locking strategies is what separates fragile prototypes from rock-solid production systems.


Buy Me A Coffee

Enjoyed this breakdown? Support my open-source work on Buy Me a Coffee or explore react-hook-lab!

Originally published on my blog. You can read the alternative breakdown here.

Top comments (0)