Node.js Worker Threads

Node.js is single-threaded, meaning all JavaScript code runs on one thread. This works perfectly for I/O-bound tasks (file reading, HTTP requests, database queries), but for CPU-intensive tasks — like complex calculations, image processing, data parsing, or cryptography — a single thread can block the event loop, making the entire application unresponsive.

Worker Threads, introduced in Node.js 10 (stable from v12), solve this by allowing multiple threads to run JavaScript code in parallel — all within the same Node.js process. Unlike child processes, worker threads share memory with the main thread, making communication faster and more efficient.

Worker Threads vs Child Processes vs Cluster

FeatureWorker ThreadsChild ProcessesCluster
Threads or ProcessesThreads (within one process)Separate OS processesSeparate OS processes
Memory SharingYes (SharedArrayBuffer)No — separate memoryNo — separate memory
Startup SpeedFast (lightweight threads)Slower (new OS process)Slower (new OS process)
Best ForCPU-intensive JavaScript tasksRunning external commands/scriptsScaling HTTP servers

When to Use Worker Threads

  • Performing complex mathematical calculations that take a long time.
  • Processing large datasets in memory.
  • Image, video, or audio processing in JavaScript.
  • Compression, encryption, or hashing of large data.
  • Parsing large JSON files or CSVs without freezing the server.

Basic Worker Thread Example

main.js – The Main Thread

const { Worker } = require('worker_threads');

console.log("Main thread started. Offloading heavy task to worker...");

const worker = new Worker('./worker.js', {
  workerData: { limit: 1000000000 } // Send data to the worker
});

// Receive results from the worker
worker.on('message', function(result) {
  console.log("Result from worker:", result);
});

// Handle errors in the worker
worker.on('error', function(err) {
  console.log("Worker error:", err.message);
});

// Handle worker exit
worker.on('exit', function(code) {
  if (code !== 0) {
    console.log("Worker stopped with exit code:", code);
  } else {
    console.log("Worker finished successfully.");
  }
});

// Main thread is NOT blocked — continues to run
console.log("Main thread: still running while worker handles the heavy task.");

worker.js – The Worker Thread Script

const { workerData, parentPort } = require('worker_threads');

const limit = workerData.limit;
let sum = 0;

// Heavy CPU computation
for (let i = 1; i <= limit; i++) {
  sum += i;
}

// Send the result back to the main thread
parentPort.postMessage({ sum, limit });

Output:

Main thread started. Offloading heavy task to worker...
Main thread: still running while worker handles the heavy task.
Result from worker: { sum: 500000000500000000, limit: 1000000000 }
Worker finished successfully.

The key here is the second line printing before the result — the main thread is not blocked.

Communicating Between Main and Worker

Communication is bidirectional. The main thread and worker can send messages to each other:

main.js

const { Worker } = require('worker_threads');

const worker = new Worker('./echo-worker.js');

// Send messages to the worker
worker.postMessage("Hello from main thread!");
worker.postMessage({ type: 'calculate', numbers: [10, 20, 30] });

// Receive responses
worker.on('message', function(msg) {
  console.log("Worker replied:", msg);
});

echo-worker.js

const { parentPort } = require('worker_threads');

parentPort.on('message', function(msg) {
  if (typeof msg === 'string') {
    parentPort.postMessage("Echo: " + msg);
  } else if (msg.type === 'calculate') {
    const sum = msg.numbers.reduce((a, b) => a + b, 0);
    parentPort.postMessage({ result: sum });
  }
});

Sharing Memory with SharedArrayBuffer

Worker threads can share memory using SharedArrayBuffer. This avoids copying data between threads — useful for very large datasets:

// main.js
const { Worker } = require('worker_threads');

const sharedBuffer = new SharedArrayBuffer(4); // 4 bytes of shared memory
const sharedArray = new Int32Array(sharedBuffer);
sharedArray[0] = 0; // Initialize counter

const worker = new Worker('./shared-worker.js', {
  workerData: { sharedBuffer }
});

worker.on('exit', function() {
  console.log("Shared counter final value:", sharedArray[0]);
});
// shared-worker.js
const { workerData } = require('worker_threads');

const sharedArray = new Int32Array(workerData.sharedBuffer);

for (let i = 0; i < 1000; i++) {
  Atomics.add(sharedArray, 0, 1); // Thread-safe increment
}

Atomics ensures that operations on shared memory are thread-safe (not interrupted by another thread mid-operation).

Worker Thread Pool – Reusing Workers

Creating a new worker for each task has overhead. In production, a pool of reusable workers is maintained:

const { Worker } = require('worker_threads');

class WorkerPool {
  constructor(workerFile, poolSize) {
    this.workers = [];
    for (let i = 0; i < poolSize; i++) {
      this.workers.push({ worker: new Worker(workerFile), busy: false });
    }
  }

  runTask(data) {
    return new Promise((resolve, reject) => {
      const available = this.workers.find(w => !w.busy);

      if (!available) {
        return reject(new Error("No available workers. Try again later."));
      }

      available.busy = true;

      available.worker.postMessage(data);

      available.worker.once('message', function(result) {
        available.busy = false;
        resolve(result);
      });

      available.worker.once('error', function(err) {
        available.busy = false;
        reject(err);
      });
    });
  }
}

Inline Workers – No Separate File Needed

Small worker code can be written inline using a URL and Blob (Node.js 16+):

const { Worker } = require('worker_threads');

const workerCode = `
  const { parentPort, workerData } = require('worker_threads');
  const result = workerData.numbers.reduce((a, b) => a + b, 0);
  parentPort.postMessage(result);
`;

const worker = new Worker(workerCode, {
  eval: true,
  workerData: { numbers: [1, 2, 3, 4, 5] }
});

worker.on('message', function(sum) {
  console.log("Sum:", sum); // Sum: 15
});

Key Points

  • Worker Threads run JavaScript in parallel threads within the same Node.js process.
  • They are ideal for CPU-intensive tasks that would otherwise block the event loop.
  • Create a worker with new Worker('./file.js', { workerData }) and communicate using postMessage() and .on('message').
  • workerData passes data from the main thread to the worker at startup (read-only in the worker).
  • parentPort.postMessage() sends data from the worker back to the main thread.
  • SharedArrayBuffer allows shared memory between threads; use Atomics for thread-safe operations.
  • Worker thread pools improve efficiency by reusing workers instead of creating new ones for each task.

Leave a Comment

Your email address will not be published. Required fields are marked *