Course contentsShow
← Node.js
Lesson 1099 of 1,98010. Real-time Communication and WebsocketsFree lesson

Preventing Race Conditions in WebSocket Handlers

You will implement locking mechanisms to ensure atomic updates to shared resources.

In this lesson, you will learn how to prevent data corruption by implementing locking mechanisms to ensure atomic updates when multiple WebSocket events interact with the same shared resource.

The Problem: Asynchronous Interference

In a Node.js WebSocket server, events are processed asynchronously. Imagine you have a shared wallet object. A user sends two rapid "deposit" messages. If your code performs a "read-modify-write" operation—where it fetches the balance, adds the amount, and saves it back—an incoming event might trigger the second update before the first has finished saving. This leads to a race condition where one deposit is overwritten and lost.

The Solution: Using an Async Mutex

To maintain consistency, we must ensure that these operations are atomic. Since Node.js is single-threaded but non-blocking, we can use a "mutex" (mutual exclusion) lock to queue operations. When a process needs to update a resource, it must "acquire" the lock. Any subsequent requests must wait until the lock is released.

Here is how you might implement a simple locking pattern:

const locks = new Map();

async function updateWallet(userId, amount) {
  // Ensure we have a lock for this specific user
  if (!locks.has(userId)) {
    locks.set(userId, Promise.resolve());
  }

  // Queue the operation behind the existing promise chain
  const previousOp = locks.get(userId);
  
  const currentOp = previousOp.then(async () => {
    const balance = await db.getBalance(userId);
    await db.setBalance(userId, balance + amount);
  });

  locks.set(userId, currentOp);
  return currentOp;
}

By chaining the promises, you force the operations to execute sequentially rather than concurrently. Even if the WebSocket handler receives five messages in a few milliseconds, the currentOp ensures the database read and write happen in a predictable order for that specific user.

While this approach works for memory-resident state or single-server instances, remember that if you move to a multi-server setup, you would eventually need to transition from an in-memory lock to a distributed locking mechanism, such as one provided by Redis.

Key Takeaway: Race conditions in WebSocket handlers occur when multiple events access shared state concurrently; using promise-based locking ensures that sensitive operations complete sequentially, preserving data integrity.