iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

worker_threads

Worker threads let Node offload CPU-bound work to a separate V8 isolate without blocking the event loop. Use them for image encoding, hashing, ZIP compression, parsing, or any pure-CPU task longer than a few milliseconds. They are not the right tool for I/O — async I/O on the main thread already scales further.

Run a CPU-bound task in a worker pool

EXAMPLE
// worker.js — runs in its own thread, communicates by messages
const { parentPort } = require('node:worker_threads');
const crypto = require('node:crypto');

parentPort.on('message', (job) => {
  // Pretend-expensive CPU work: bcrypt-style salted hash
  const salt = crypto.randomBytes(16);
  let hash = Buffer.from(job.password, 'utf8');
  for (let i = 0; i < 100_000; i++) {
    hash = crypto.createHash('sha256').update(hash).update(salt).digest();
  }
  parentPort.postMessage({ id: job.id, hash: hash.toString('hex') });
});

// pool.js — a small fixed-size worker pool
const { Worker } = require('node:worker_threads');
const os = require('node:os');

class WorkerPool {
  constructor(file, size = os.availableParallelism()) {
    this.workers = Array.from({ length: size }, () => ({
      w: new Worker(file),
      busy: false,
    }));
    this.queue = [];
  }

  run(job) {
    return new Promise((resolve, reject) => {
      const task = { job, resolve, reject };
      const idle = this.workers.find((x) => !x.busy);
      if (idle) this._dispatch(idle, task);
      else this.queue.push(task);
    });
  }

  _dispatch(slot, task) {
    slot.busy = true;
    const onMsg = (msg) => {
      slot.w.off('message', onMsg);
      slot.w.off('error', onErr);
      slot.busy = false;
      task.resolve(msg);
      const next = this.queue.shift();
      if (next) this._dispatch(slot, next);
    };
    const onErr = (e) => { slot.w.off('message', onMsg); slot.busy = false; task.reject(e); };
    slot.w.once('message', onMsg);
    slot.w.once('error', onErr);
    slot.w.postMessage(task.job);
  }

  async close() {
    await Promise.all(this.workers.map((x) => x.w.terminate()));
  }
}

// main.js
const pool = new WorkerPool(require.resolve('./worker.js'), 4);
const results = await Promise.all(
  Array.from({ length: 20 }, (_, i) => pool.run({ id: i, password: 'hunter2' }))
);
console.log(results[0]);
await pool.close();

Why it matters

A pool of size N = availableParallelism() is the sweet spot — more workers than cores just adds context-switch overhead. Measure: if your task is shorter than ~1ms, the postMessage round-trip will eat the gain and a plain async call is faster.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// worker.js
import { parentPort } from 'node:worker_threads';
parentPort.on('message', n => parentPort.postMessage(n * 2));
Try it Yourself »

Discussion

Loading…