JavaScript Worker Threads: Multithreading in Node.js

Anish RoyAnish Roy
5 min read

JavaScript was designed to be single-threaded. For most use cases — especially in web development — that’s not a problem. The event loop, combined with asynchronous programming, handles I/O operations efficiently without blocking the main thread.

But once your application starts doing CPU-intensive work — things like image processing, data parsing, or running complex algorithms — that single thread becomes a bottleneck. The event loop stalls, your app becomes unresponsive, and performance tanks.

That’s where Node.js worker threads come in.


Understanding the Basics

JavaScript’s Single Thread Model

JavaScript runs in a single-threaded environment. That means one call stack, one thread of execution. While asynchronous operations like file reads or HTTP requests don’t block the thread directly, they’re handled by the system (via libuv in Node.js), and their results are queued for the event loop to process later.

But all JavaScript code — including callbacks — still runs on that one thread. If you run a heavy computation (like calculating Fibonacci numbers recursively), it blocks everything else. Async programming doesn’t help here.

Introducing worker_threads

Starting in Node.js v10.5 (and stabilized in v12), the worker_threads module allows JavaScript to run CPU-heavy code in parallel — across multiple threads.

Each worker:

  • Runs in its own thread with a separate event loop and memory space.

  • Communicates with the main thread via messages (or shared memory using SharedArrayBuffer).

  • Is lightweight compared to a full child_process, but still isolated.

This allows Node.js applications to finally offload CPU-bound tasks without freezing the main thread.


Async Programming Is Not Multithreading

To be clear: asynchronous programming and multithreading are not the same.

FeatureAsynchronous (Event Loop)Multithreading (worker_threads)
Thread CountSingleMultiple
Designed ForI/O-bound tasksCPU-intensive tasks
Parallel ExecutionNoYes
Memory SharingNoPossible (via SharedArrayBuffer)
Examplesfetch, fs.readFileImage processing, math-heavy logic

Async is efficient for waiting — like network requests. But if you're trying to compute something heavy, you need threads.


Benchmarking: Single Thread vs Worker Threads

To show the difference, let’s run the same task — calculating fibonacci(45) five times — using both approaches.

Single-threaded Example

const fib = (n) => (n <= 1 ? n : fib(n - 1) + fib(n - 2));
console.time("SingleThread");

for (let i = 0; i < 5; i++) {
  console.log(fib(45));
}

console.timeEnd("SingleThread");

Time taken: ~73 seconds

The main thread handles all the work, sequentially.

Multi-threaded Example with 5 Workers

main.js

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

console.time("Multi-Worker");

let completed = 0;

for (let i = 0; i < 5; i++) {
  const worker = new Worker('./worker.js');
  worker.on('message', (msg) => {
    console.log(msg);
    completed++;
    if (completed === 5) {
      console.timeEnd("Multi-Worker");
    }
  });
}

worker.js

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

function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

const result = fib(45);
parentPort.postMessage(result);

Time taken: ~20 seconds

Five threads doing the work in parallel — a clear performance boost.


Building a Worker Pool for Better Efficiency

Spawning a new worker thread every time you need one is not scalable. Threads are expensive to create and destroy. That’s why you build a worker pool — a fixed number of threads that are reused for multiple tasks.

Why Use a Pool?

  • Reduces the overhead of creating/destroying threads.

  • Keeps control over concurrency.

  • Handles task queuing when all workers are busy.

How It Works

The main thread:

  • Initializes a pool of workers.

  • Receives tasks and either assigns them to an idle worker or queues them.

  • Reuses idle threads for upcoming tasks.


Worker Pool Example

main.js

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

const taskQueue = [];
const workers = [];

class PooledWorker {
  constructor(id) {
    this.id = id;
    this.busy = false;
    this.worker = new Worker('./worker.js');

    this.worker.on('message', ({ status, result }) => {
      if (status === "Complete") {
        console.log(`Worker ${this.id} done. Result: ${result}`);
        this.busy = false;
        if (taskQueue.length > 0) {
          const next = taskQueue.shift();
          this.runTask(next);
        }
      }
    });

    this.worker.on('error', (err) => {
      console.error(`Worker ${this.id} error:`, err);
    });
  }

  runTask(num) {
    this.busy = true;
    this.worker.postMessage({ num });
  }
}

for (let i = 0; i < POOL_SIZE; i++) {
  workers.push(new PooledWorker(i));
}

function assignTask(num) {
  const idle = workers.find(w => !w.busy);
  if (idle) {
    idle.runTask(num);
  } else {
    console.log(`All workers busy. Queuing Fibonacci(${num})`);
    taskQueue.push(num);
  }
}

// Simulate incoming tasks
setInterval(() => {
  const num = Math.floor(Math.random() * 30 + 20);
  assignTask(num);
}, 400);

worker.js

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

function fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}

parentPort.on('message', ({ num }) => {
  const result = fib(num);
  parentPort.postMessage({ status: "Complete", result });
});

This system handles bursts of CPU-bound tasks efficiently, while maintaining predictable performance.


Final Takeaways

  • JavaScript’s event loop handles I/O well, but it can’t handle CPU-heavy logic without blocking.

  • worker_threads provide real multithreading for Node.js, ideal for tasks like image processing, complex algorithms, and large data transformations.

  • Benchmarks show clear performance improvements when using workers.

  • A worker pool is the optimal pattern for managing threads in production environments.

  • Use async for I/O. Use threads for computation. Know the difference.

When you understand the limits of the event loop — and how to work around them with threads — you stop treating Node.js like a toy. You start building real, scalable systems.

1
Subscribe to my newsletter

Read articles from Anish Roy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anish Roy
Anish Roy