Real Threads for CPU-Heavy Work
Worker Threads
worker_threads lets you run JS in a separate thread. Use for CPU-heavy tasks that would otherwise block the event loop.
What you'll learn
- Spawn a Worker
- Send and receive messages
- Know when (not) to reach for one
Node’s single thread is great for IO. For CPU-heavy work, it becomes a problem — your server stops responding while it hashes a 1GB file.
Worker threads are the fix. Real threads, shared memory option, message passing.
Basic Worker
// worker.mjs
import { parentPort } from "node:worker_threads";
parentPort.on("message", (n) => {
let total = 0;
for (let i = 0; i < n; i++) total += i;
parentPort.postMessage(total);
}); // main.mjs
import { Worker } from "node:worker_threads";
const worker = new Worker("./worker.mjs");
worker.on("message", (result) => {
console.log("sum =", result);
worker.terminate();
});
worker.postMessage(1_000_000_000); // ask for sum of 1B numbers
// main thread is FREE during the computation
console.log("main is doing other things"); Main spawns a worker, sends a message, keeps going. Worker does the work in a separate OS thread.
Inline Worker
Tiny workers don’t need their own file:
import { Worker } from "node:worker_threads";
const code = `
const { parentPort } = require("node:worker_threads");
parentPort.on("message", (n) => parentPort.postMessage(n * 2));
`;
const w = new Worker(code, { eval: true });
w.on("message", console.log);
w.postMessage(21); // logs 42 Cost / Benefit
Spawning a worker takes time — easily 10–50ms. Not worth it for small tasks. Use for:
- Image processing
- Heavy crypto / compression
- Large JSON parsing
- ML inference
Don’t use for:
- One database query
- File reads (use async fs — IO doesn’t block)
- HTTP calls
Worker Pools
Spinning up a worker per request kills the benefit. Pool them:
piscina is the standard
worker-pool library.
import Piscina from "piscina";
const pool = new Piscina({ filename: "./worker.mjs" });
const result = await pool.run({ n: 1_000_000 }); Shared Memory
Workers can share SharedArrayBuffer. Powerful, but you’re now in
shared-state concurrency territory — atomics, locks, the works.
Most apps never need it.