Concurrency & Parallelism

Threads vs async/await vs processes, the event loop, race conditions, locks, optimistic concurrency, and designing systems that safely handle concurrent requests.

Advanced · 17 min read

Concurrency vs Parallelism

Concurrency Parallelism
Multiple tasks making progress (interleaved) Multiple tasks executing simultaneously
Single CPU, time-sliced Multiple CPU cores
Node.js event loop, goroutines Worker threads, multi-process, SIMD
I/O-bound work benefits most CPU-bound work benefits most
"Dealing with many things at once" "Doing many things at once"

Race Conditions

A race condition occurs when the correctness of a result depends on the relative timing of concurrent operations. The classic example is two requests both reading a balance, both seeing $100, both debiting $60, and both succeeding — leaving a negative balance.

// ❌ Race condition: two requests both read balance = 100
async function withdrawUnsafe(userId: string, amount: number) {
  const { balance } = await db.query('SELECT balance FROM accounts WHERE id = $1', [userId]);
  if (balance < amount) throw new Error('Insufficient funds');
  await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, userId]);
}

// ✅ Option 1: Atomic update with constraint check
async function withdrawAtomic(userId: string, amount: number) {
  const result = await db.query(
    'UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1 RETURNING balance',
    [amount, userId]
  );
  if (result.rowCount === 0) throw new Error('Insufficient funds');
}

// ✅ Option 2: Optimistic locking with version field
async function withdrawOptimistic(userId: string, amount: number) {
  const { balance, version } = await db.query('SELECT balance, version FROM accounts WHERE id = $1', [userId]);
  if (balance < amount) throw new Error('Insufficient funds');
  const result = await db.query(
    'UPDATE accounts SET balance = $1, version = version + 1 WHERE id = $2 AND version = $3',
    [balance - amount, userId, version]
  );
  if (result.rowCount === 0) throw new Error('Concurrent update detected — retry');
}

Distributed Locks

In distributed systems, a mutex inside one process doesn't prevent another server from running the same code. Use Redis SETNX (or Redlock) for distributed locking.

CAUTION: Distributed locks are not a silver bullet. Clock drift, network partitions, and GC pauses can all cause lock expiry before work completes. Prefer idempotent operations + optimistic concurrency over distributed locks when possible.


Part of the System Design series on Tekivex. Browse all tutorials or explore our open-source products.