Worker Threads for CPU-Bound Algorithms

Offload Heavy Sorts and Transforms to a True Parallel Thread

Worker Threads for CPU-Bound Algorithms

node:worker_threads lets you run CPU-intensive algorithms on a separate OS thread, keeping the main event loop free — learn message passing, transferable buffers, and when SharedArrayBuffer is worth the complexity.

6 min read Level 3/5 #dsa#nodejs#interview
What you'll learn
  • Spawn a worker thread and send work to it via postMessage
  • Return results back to the main thread using the message event
  • Choose between message-copy semantics and SharedArrayBuffer for large data

The event-loop chunking trick from the previous lesson still runs on one CPU core. When you need to sort a 10-million-element array, process an image buffer, or run a graph algorithm against a large dataset, you want a second OS thread — and node:worker_threads provides exactly that.

Spawning a Worker

// main.mjs
import { Worker, isMainThread, parentPort, workerData }
  from 'node:worker_threads';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);

if (isMainThread) {
  const data = Array.from({ length: 1_000_000 }, () => Math.random());

  const worker = new Worker(__filename, { workerData: data });

  worker.once('message', (sorted) => {
    console.log('First element after sort:', sorted[0]);
  });

  worker.once('error', (err) => console.error(err));
} else {
  // Worker thread: sort and send back
  const result = workerData.slice().sort((a, b) => a - b);
  parentPort.postMessage(result);
}

workerData is structured-cloned when the thread starts — a full copy. postMessage also clones by default. Cloning is safe but costs memory and CPU proportional to data size.

Transferable Buffers (Zero-Copy)

For large binary data, transfer ownership of an ArrayBuffer instead of copying it. The sender can no longer access it after the transfer.

// main.mjs (transfer path)
import { Worker } from 'node:worker_threads';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);

const buf = new ArrayBuffer(1024 * 1024 * 64); // 64 MB
const worker = new Worker(__filename, {
  workerData: buf,
  transferList: [buf],              // transfer, not copy
});

// buf is now detached here — accessing it throws

SharedArrayBuffer for Bidirectional Sharing

SharedArrayBuffer lets both threads read and write the same memory without any message passing. Use Atomics to coordinate:

// Both threads share this buffer — no copies, no messages for data
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 4);
const flag = new Int32Array(shared);

// Worker signals it is done by writing 1 to index 0
Atomics.store(flag, 0, 1);
Atomics.notify(flag, 0, 1);

// Main thread waits (do NOT use Atomics.wait on the main thread in production
// — it blocks; use Atomics.waitAsync instead)
Atomics.waitAsync(flag, 0, 0).value.then(() => {
  console.log('Worker finished, flag is:', Atomics.load(flag, 0));
});

Choosing a Data Passing Strategy

StrategyBest forGotcha
workerData cloneSmall startup configFull copy on start
postMessage cloneGeneral messagesCopy cost per message
Transferable ArrayBufferLarge binary, one-waySender loses access
SharedArrayBufferHigh-frequency bidirectionalNeeds Atomics for safety

Worker Pool Pattern

For a server handling many requests, spawning a worker per request is expensive. Maintain a pool of N workers (typically os.cpus().length - 1) and queue tasks to idle workers. Libraries like piscina implement this pattern and are production-ready.

Up Next

When your data is too big for memory altogether, streams let you process it piece by piece without a worker thread at all.

Streams for Large Data Processing →