Node.js Worker Threads: Unlocking True Multithreading

Mar 13, 2025


When people hear "Node.js is single-threaded", they assume it can’t do parallel processing. But that’s only half true! While JavaScript itself runs on a single thread, Node.js provides a Worker Threads API that allows real multithreading.

Today, we’ll break down how Worker Threads work, when to use them, and how they compare to Clustering. πŸš€


πŸ”₯ Why Do We Need Worker Threads?

The Cluster module (which we covered in this post lets us scale an application across multiple CPU cores. But there’s a problem:

❌ Each worker is a separate process – They don’t share memory.
❌ High memory usage – Each worker has its own event loop.
❌ Not ideal for CPU-heavy tasks – Computation-heavy tasks block execution.

Solution? πŸ‘‰ Worker Threads allow you to run CPU-intensive tasks in the background without blocking the main thread.


βš™οΈ What Happens Under the Hood?

When you create a Worker Thread, Node.js spins up a new thread inside the process using the libuv thread pool (which normally handles I/O operations). Unlike Clustering (which spawns separate processes), Worker Threads:

  • Share memory with the main thread using SharedArrayBuffer.
  • Use worker_threads to communicate instead of IPC (Inter-Process Communication).
  • Run JavaScript in true parallel execution instead of relying on the Event Loop.

Key Differences from the Main Thread

Main Thread πŸ—

  • Runs JavaScript? βœ… Yes
  • Has its own event loop? βœ… Yes
  • Shares memory? ❌ No
  • Best for? I/O, handling requests

Worker Thread πŸ› 

  • Runs JavaScript? βœ… Yes
  • Has its own event loop? βœ… Yes
  • Shares memory? βœ… Yes
  • Best for? CPU-heavy tasks

πŸš€ How Do Worker Threads Work?

A Worker Thread runs JavaScript in parallel to the main event loop, sharing the same memory.

Example: Spawning a Worker

Let’s start with a simple worker thread that calculates Fibonacci numbers.

Step 1: Create the worker file (worker.mjs)

import { parentPort } from "worker_threads";

function fibonacci(n) {
  return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

// Listen for messages from the main thread
parentPort.on("message", (num) => {
  const result = fibonacci(num);
  parentPort.postMessage(result);
});

Step 2: Use the Worker Thread in main.mjs

import { Worker } from "worker_threads";

function runWorker(input) {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./worker.mjs", { workerData: input });

    worker.on("message", resolve);
    worker.on("error", reject);
    worker.on("exit", (code) => {
      if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
    });
  });
}

(async () => {
  console.log("πŸš€ Main thread running...");
  const result = await runWorker(40); // Compute Fibonacci(40)
  console.log("βœ… Worker result:", result);
})();

πŸ›  Key Features of Worker Threads

βœ… Runs JavaScript in true parallel threads.
βœ… Shares memory between threads using SharedArrayBuffer.
βœ… Ideal for CPU-intensive tasks like encryption, image processing, and complex calculations.


⚑ Real-World Use Case: Image Processing

Let’s say we’re building an API that resizes images. A single-threaded Node.js server would struggle with multiple requests. Instead, we can use Worker Threads for processing.

Step 1: Create the worker file (image-worker.mjs)

import { parentPort, workerData } from "worker_threads";
import sharp from "sharp"; // Image processing library

async function resizeImage({ inputPath, outputPath }) {
  await sharp(inputPath).resize(300, 300).toFile(outputPath);
  parentPort.postMessage(`Image processed: ${outputPath}`);
}

resizeImage(workerData);

Step 2: Spawn the worker in server.mjs

import express from "express";
import { Worker } from "worker_threads";

const app = express();

app.get("/resize", (req, res) => {
  const worker = new Worker("./image-worker.mjs", {
    workerData: { inputPath: "input.jpg", outputPath: "output.jpg" },
  });

  worker.on("message", (msg) => res.send(msg));
  worker.on("error", (err) => res.status(500).send(err.message));
});

app.listen(3000, () => console.log("πŸš€ Server running on port 3000"));

How This Helps:

βœ… Non-blocking image processing – API stays responsive.
βœ… Multiple requests handled in parallel – Unlike a single-threaded server.
βœ… CPU-efficient – Offloads processing to workers.


πŸ”„ Worker Threads vs. Cluster

Worker Threads πŸ› :

  • Thread-based: βœ… Yes
  • Memory sharing: βœ… Yes
  • Best for: CPU-intensive tasks
  • Multiple cores: βœ… Yes
  • Communication: Fast (shared memory)

Cluster πŸš€:

  • Thread-based: ❌ No (process-based)
  • Memory sharing: ❌ No (each worker is isolated)
  • Best for: High concurrency
  • Multiple cores: βœ… Yes
  • Communication: Slower (inter-process messaging)

When to Use What?

  • Use Cluster β†’ If you're handling high concurrency (e.g., an Express API).
  • Use Worker Threads β†’ If you need to process heavy calculations (e.g., AI, encryption, image processing).
  • Combine both β†’ Cluster for handling requests + Worker Threads for CPU-heavy tasks.

🎯 Final Thoughts

Worker Threads give Node.js true multithreading, making it faster for CPU-heavy tasks. πŸš€

  • Cluster = More processes (good for web servers).
  • Worker Threads = More threads (good for computation).
diogoaoliveira.com