Worker Threads

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.

4 min read Level 3/5 #nodejs#workers#threads
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.

child_process →