Skip to content

Node.js Worker Threads — Complete Guide to Multithreading and Parallel Processing

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Node.js Worker Threads. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js worker threads enable running JavaScript code in parallel threads within the same Process, ideal for CPU-intensive tasks without blocking the event loop.

What You'll Learn

By the end of this tutorial, you'll create worker threads, communicate between main and worker threads, share memory, handle thread errors, and parallelize CPU-heavy computations.

Why Worker Threads Matter

Node.js is single-threaded for JavaScript execution. CPU-intensive operations (image processing, data Parsing, cryptography) block the event loop. Worker threads offload these tasks to separate threads.

Real-World Use

A PDF generation service uses worker threads to render documents in parallel. While one worker generates a 100-page PDF, the main thread continues serving HTTP requests without delay.

Worker Threads Learning Path

flowchart LR
  A[Cluster] --> B[Worker Threads]
  B --> C[Error Handling]
  C --> D[Debugging]
  D --> E[Express.js]
  A --> F{You Are Here}
  style F fill:#f90,color:#fff

Creating a Worker Thread

// main.js
import { Worker } from "node:worker_threads";
const worker = new Worker("./worker.js", { workerData: { count: 10 } });
worker.on("message", (result) => console.log("Result:", result));
worker.on("error", (err) => console.error("Worker error:", err.message));
worker.on("exit", (code) => console.log(`Worker exited with code ${code}`));

// worker.js
import { parentPort, workerData } from "node:worker_threads";
let sum = 0;
for (let i = 0; i < workerData.count * 1000000; i++) {
  sum += i;
}
parentPort.postMessage(sum);

Communication Between Threads

// main.js
import { Worker } from "node:worker_threads";
const worker = new Worker("./message-worker.js");
worker.postMessage({ cmd: "process", data: [1, 2, 3, 4, 5] });
worker.on("message", (msg) => console.log("Worker says:", msg));

// message-worker.js
import { parentPort } from "node:worker_threads";
parentPort.on("message", (msg) => {
  if (msg.cmd === "process") {
    const result = msg.data.map(x => x * x);
    parentPort.postMessage({ result });
  }
});

SharedArrayBuffer

For high-performance data sharing without message copying.

// main.js
import { Worker } from "node:worker_threads";
const buffer = new SharedArrayBuffer(4 * 1024);
const view = new Int32Array(buffer);
view[0] = 42;
const worker = new Worker("./shared-worker.js", { workerData: buffer });
setTimeout(() => console.log("Value from worker:", view[0]), 1000);

// shared-worker.js
import { workerData, parentPort } from "node:worker_threads";
const view = new Int32Array(workerData);
view[0] = view[0] * 2;
parentPort.postMessage("done");

Worker Pool Pattern

Managing multiple workers efficiently.

import { Worker } from "node:worker_threads";
import { cpus } from "node:os";
class WorkerPool {
  constructor(workerFile, poolSize = cpus().length) {
    this.workers = [];
    this.queue = [];
    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerFile);
      worker.on("message", (result) => {
        worker.busy = false;
        worker.resolve(result);
        this.processQueue();
      });
      worker.busy = false;
      this.workers.push(worker);
    }
  }
  runTask(data) {
    return new Promise((resolve) => {
      this.queue.push({ data, resolve });
      this.processQueue();
    });
  }
  processQueue() {
    const available = this.workers.find(w => !w.busy);
    if (available && this.queue.length) {
      available.busy = true;
      const task = this.queue.shift();
      available.resolve = task.resolve;
      available.postMessage(task.data);
    }
  }
}

Error Handling in Workers

// main.js
const worker = new Worker("./failing-worker.js");
worker.on("error", (err) => {
  console.error("Worker crashed:", err.message);
});
worker.on("exit", (code) => {
  if (code !== 0) console.error(`Worker failed with code ${code}`);
});

// failing-worker.js
import { parentPort } from "node:worker_threads";
try {
  throw new Error("Something broke");
} catch (err) {
  parentPort.postMessage({ error: err.message });
}

Common Mistakes

1. Creating Too Many Workers

Each worker thread has overhead. Creating more workers than CPU cores causes context switching overhead.

2. Blocking Workers with Heavy Operations

Workers run JavaScript, not native threads for I/O. Heavy synchronous operations still block the worker's event loop.

3. Not Handling Worker Errors

Unhandled errors in workers crash the thread. Always listen for 'error' events and handle try/catch in workers.

4. Transferring Large Data by Copy

postMessage copies data by default. Use transferList or SharedArrayBuffer for large data to avoid memory overhead.

5. Using Workers for I/O Operations

I/O operations (file reads, HTTP requests) are already non-blocking. Workers are for CPU-bound tasks only.

Practice Questions

1. What is the difference between worker_threads and cluster?

Worker threads share the same process (shared memory, less isolation). Cluster forks separate processes (isolated memory, more overhead).

2. When should you use a worker thread?

For CPU-intensive tasks like image processing, data compression, JSON parsing, or complex calculations that would block the event loop.

3. How do you share memory between threads?

Use SharedArrayBuffer. It lets multiple threads read and write the same memory without Serialization overhead.

4. What happens if a worker thread throws an unhandled error?

The worker emits an 'error' event and terminates. The main thread can catch it and spawn a replacement.

5. Challenge: Create a worker pool that processes an array of numbers in parallel, computing Fibonacci for each.

// fib-worker.js
import { parentPort } from "node:worker_threads";
function fib(n) { return n <= 1 ? n : fib(n - 1) + fib(n - 2); }
parentPort.on("message", (n) => parentPort.postMessage(fib(n)));

// main.js — pool of 4 workers computing fib(40) for 8 inputs
// (pool implementation from above; runTask sends each number)

FAQ

Are worker threads real OS threads?

Yes. They use libuv's thread pool and run JavaScript in separate V8 isolates with separate event loops.

Can worker threads access the file system?

Yes, but I/O is already non-blocking in Node.js. Workers shine for CPU work, not I/O.

What is the memory cost of a worker thread?

Each worker has a separate V8 isolate (~4-10 MB baseline). Actual usage depends on the workload.

Can workers use Native addons?

Yes, native addons loaded in the main thread are also available in workers, subject to addon compatibility.

Is there a limit on worker thread count?

The V8 option --max-old-space-size applies per worker. Total memory depends on available system RAM.

Mini Project: Image Resizer

Create a worker thread that resizes images using sharp, keeping the main thread responsive.

// resize-worker.js
import { parentPort, workerData } from "node:worker_threads";
import sharp from "sharp";
const { inputPath, outputPath, width, height } = workerData;
sharp(inputPath).resize(width, height).toFile(outputPath, (err) => {
  parentPort.postMessage(err ? { error: err.message } : { success: true });
});

What's Next

Node.js Error Handling Node.js Testing Express.js

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro