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).