Node.js: Worker Threads

Last updated: 2026-08-26

1. What You'll Learn



2. Story: Alice's Image Processing Acceleration

Alice is in charge of image processing services at a SaaS company. When users upload images, the system needs to generate thumbnails in three different sizes for each image. The original single-threaded solution took about 30 seconds to process 100 images, and during peak hours, there was a significant backlog of requests.

She decided to use worker threads to distribute the tasks across four threads for parallel processing: the main thread reads the list of files and passes task parameters via workerData, and the worker threads return the results via parentPort after generating the thumbnails. Ultimately, the processing time for 100 images was reduced to about 8 seconds, and throughput increased nearly fourfold.



3. Worker Class Basics

The worker_threads module provides true multithreading capabilities in Node.js. Each Worker executes scripts in a separate thread and has its own V8 engine instance and event loop.

JAVASCRIPT
const { Worker } = require('worker_threads');

const worker = new Worker('./heavy-task.js', {
  workerData: { taskId: 42, input: 'hello' }
});

worker.on('message', (result) => {
  console.log('Result from worker:', result);
});

worker.on('error', (err) => {
  console.error('Worker error:', err);
});

worker.on('exit', (code) => {
  if (code !== 0) {
    console.error(`Worker stopped with exit code ${code}`);
  }
});

Inside the Worker script:

JAVASCRIPT
// heavy-task.js
const { parentPort, workerData } = require('worker_threads');

const { taskId, input } = workerData;

const result = performHeavyComputation(input);

parentPort.postMessage({ taskId, result });

function performHeavyComputation(data) {
  let hash = 0;
  for (let i = 0; i < 1e8; i++) {
    hash = (hash + data.charCodeAt(i % data.length)) % 65536;
  }
  return hash;
}

▶ Example: Creating and Communicating with a Basic Worker

JAVASCRIPT
// main.js
const { Worker } = require('worker_threads');

const worker = new Worker(`
  const { parentPort } = require('worker_threads');
  parentPort.postMessage('Hello from worker!');
`, { eval: true });

worker.on('message', (msg) => {
  console.log(msg); // Hello from worker!
});
▶ Try it Yourself

4. parentPort Bidirectional Communication

parentPort is the message channel between the Worker and the main thread. The main thread sends messages via worker.postMessage(), and the Worker replies via parentPort.postMessage(). Both sides listen for message events to receive data.

▶ Example: Two-way Message Exchange

JAVASCRIPT
// main.js
const { Worker } = require('worker_threads');

const worker = new Worker('./echo-worker.js');

worker.postMessage({ action: 'greet', name: 'Alice' });

worker.on('message', (msg) => {
  console.log('Main received:', msg);
  if (msg.action === 'greet_reply') {
    worker.postMessage({ action: 'task', payload: 100 });
  }
});
▶ Try it Yourself
JAVASCRIPT
// echo-worker.js
const { parentPort } = require('worker_threads');

parentPort.on('message', (msg) => {
  if (msg.action === 'greet') {
    parentPort.postMessage({
      action: 'greet_reply',
      text: `Hello, ${msg.name}!`
    });
  } else if (msg.action === 'task') {
    const result = msg.payload * 2;
    parentPort.postMessage({ action: 'result', value: result });
  }
});

▶ Example: Zero-copy transfer of Transferable objects

JAVASCRIPT
const { Worker } = require('worker_threads');

const buffer = new ArrayBuffer(1024 * 1024); // 1 MB

const worker = new Worker('./process-buffer.js');

worker.postMessage({ buffer }, [buffer]);

console.log('Buffer transferred, main thread no longer owns it');
▶ Try it Yourself
JAVASCRIPT
// process-buffer.js
const { parentPort, workerData } = require('worker_threads');

parentPort.on('message', ({ buffer }) => {
  const view = new Uint8Array(buffer);
  view[0] = 42;
  parentPort.postMessage({ done: true, firstByte: view[0] }, [buffer]);
});


5. workerData Initialization Data

workerData is read-only initial data passed via options when creating a Worker. The Worker reads this data directly internally, without the need for message passing. It is suitable for passing configuration, file paths, task parameters, and so on.

▶ Example: Batch Image Processing Worker

JAVASCRIPT
// main.js
const { Worker } = require('worker_threads');
const path = require('path');

const imageFiles = [
  'photo-001.jpg', 'photo-002.jpg', 'photo-003.jpg',
  'photo-004.jpg', 'photo-005.jpg', 'photo-006.jpg'
];

const WORKER_COUNT = 4;
const chunkSize = Math.ceil(imageFiles.length / WORKER_COUNT);

for (let i = 0; i < WORKER_COUNT; i++) {
  const chunk = imageFiles.slice(i * chunkSize, (i + 1) * chunkSize);
  const worker = new Worker('./image-worker.js', {
    workerData: {
      workerId: i,
      files: chunk,
      outputDir: './thumbnails'
    }
  });

  worker.on('message', (result) => {
    console.log(`Worker ${i} done:`, result.processed);
  });
}
▶ Try it Yourself
JAVASCRIPT
// image-worker.js
const { parentPort, workerData } = require('worker_threads');
const path = require('path');

const { workerId, files, outputDir } = workerData;

async function generateThumbnail(file) {
  // Simulate image processing
  return new Promise((resolve) => {
    setTimeout(() => resolve(`${file} -> thumb`), 100);
  });
}

(async () => {
  const processed = [];
  for (const file of files) {
    const result = await generateThumbnail(file);
    processed.push(result);
  }
  parentPort.postMessage({ workerId, processed });
})();


6. MessageChannel and MessagePort

MessageChannel Create a pair of interconnected MessagePort instances that can be assigned to different workers, enabling direct communication between threads without going through the main thread.

100%
graph LR
    Main["Main Thread"] -->|"worker.postMessage()"| W1["Worker 1"]
    W1 -->|"parentPort.postMessage()"| Main
    Main -->|"worker.postMessage()"| W2["Worker 2"]
    W2 -->|"parentPort.postMessage()"| Main
    W1 <-->|"MessagePort"| W2

▶ Example: Two Workers Communicating Directly

JAVASCRIPT
// main.js
const { Worker, MessageChannel } = require('worker_threads');

const worker1 = new Worker('./chat-worker.js', {
  workerData: { id: 1 }
});
const worker2 = new Worker('./chat-worker.js', {
  workerData: { id: 2 }
});

const { port1, port2 } = new MessageChannel();

worker1.postMessage({ port: port1 }, [port1]);
worker2.postMessage({ port: port2 }, [port2]);
▶ Try it Yourself
JAVASCRIPT
// chat-worker.js
const { parentPort, workerData } = require('worker_threads');

parentPort.once('message', ({ port }) => {
  port.on('message', (msg) => {
    console.log(`Worker ${workerData.id} received:`, msg);
  });

  setInterval(() => {
    port.postMessage(`Hello from Worker ${workerData.id}`);
  }, 1000);
});


7. SharedArrayBuffer and Atomics

SharedArrayBuffer Allows multiple threads to share the same block of memory. When used in conjunction with the atomic operations provided by Atomics, it enables safe reading and writing of shared data, thereby avoiding race conditions.

▶ Example: Shared Counter

JAVASCRIPT
// main.js
const { Worker } = require('worker_threads');

const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

const WORKER_COUNT = 4;
const INCREMENTS_PER_WORKER = 100000;

for (let i = 0; i < WORKER_COUNT; i++) {
  const worker = new Worker('./counter-worker.js', {
    workerData: { sharedBuffer, increments: INCREMENTS_PER_WORKER }
  });

  worker.on('exit', () => {
    const final = Atomics.load(sharedArray, 0);
    console.log(`Final counter value: ${final}`);
  });
}
▶ Try it Yourself
JAVASCRIPT
// counter-worker.js
const { parentPort, workerData } = require('worker_threads');

const { sharedBuffer, increments } = workerData;
const sharedArray = new Int32Array(sharedBuffer);

for (let i = 0; i < increments; i++) {
  Atomics.add(sharedArray, 0, 1);
}

parentPort.postMessage('done');


8. Comparison of Communication Methods

Communication Method Direction Features Applicable Scenarios
parentPort Main Thread ↔ Worker Bidirectional messaging, structured cloning General task communication
workerData Main thread → Worker Read-only; passed in upon creation Initialization configuration/parameters
MessageChannel Worker ↔ Worker Direct connection between ports, bypassing the main thread Inter-thread collaboration
SharedArrayBuffer Shared by all threads Zero-copy, requires Atomics High-frequency data exchange


9. Worker vs child_process vs cluster

Feature Worker Threads child_process cluster
Unit Threads Processes Processes
Memory Shared process memory Independent memory space Independent memory space
Communication Messages / Shared Memory IPC Serialization IPC Serialization
Startup Overhead Moderate High High
Applicable CPU-intensive computing Standalone programs/sandboxes Multi-core HTTP services
Stability Worker crashes affect the same process Child process crashes do not affect each other Worker process crashes do not affect each other
Shared Status SharedArrayBuffer Not supported Not supported


10. Tasks Suitable and Unsuitable for Multithreading

Suitable for multithreading Not suitable for multithreading
Image Processing/Thumbnail Generation Simple HTTP Request Routing
Encryption/Hash Calculations Database CRUD Operations
Compression/Decompression File I/O (asynchronous is sufficient)
Large-scale mathematical operations Simple JSON conversions
PDF Generation/Rendering Short-Lived Microtasks
Audio and Video Transcoding Event-Driven Message Forwarding


11. Thread Pool Pattern

Creating a Worker incurs some overhead, and frequently creating and destroying them wastes resources. A thread pool maintains a fixed number of Workers; when a task arrives, it is assigned to an idle Worker, and once the task is complete, the Worker is reclaimed and reused.

▶ Example: Building a Simple Thread Pool from Scratch

JAVASCRIPT 📖 Display only
// pool.js
const { Worker } = require('worker_threads');

class Pool {
  constructor(workerFile, size) {
    this.workerFile = workerFile;
    this.size = size;
    this.workers = [];
    this.queue = [];

    for (let i = 0; i < size; i++) {
      const worker = new Worker(workerFile);
      worker.busy = false;
      worker.on('message', (result) => {
        const task = worker.currentTask;
        worker.busy = false;
        worker.currentTask = null;
        task.resolve(result);
        this._processQueue();
      });
      worker.on('error', (err) => {
        const task = worker.currentTask;
        if (task) {
          worker.busy = false;
          worker.currentTask = null;
          task.reject(err);
          this._processQueue();
        }
      });
      this.workers.push(worker);
    }
  }

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

  _assign(worker, task) {
    worker.busy = true;
    worker.currentTask = task;
    worker.postMessage(task.data);
  }

  _processQueue() {
    if (this.queue.length === 0) return;
    const idle = this.workers.find((w) => !w.busy);
    if (!idle) return;
    const task = this.queue.shift();
    this._assign(idle, task);
  }

  destroy() {
    for (const worker of this.workers) {
      worker.terminate();
    }
    this.workers = [];
    this.queue = [];
  }
}

module.exports = Pool;
61 logic lines (exceeds 40-line limit, display only)

▶ Example: Calculating the Fibonacci sequence using a thread pool

JAVASCRIPT
// main.js
const Pool = require('./pool.js');

const pool = new Pool('./fib-worker.js', 4);

async function main() {
  const tasks = [40, 41, 42, 43, 44, 45, 46, 47];

  const promises = tasks.map((n) => pool.run({ n }));

  const results = await Promise.all(promises);

  for (let i = 0; i < tasks.length; i++) {
    console.log(`fib(${tasks[i]}) = ${results[i]}`);
  }

  pool.destroy();
}

main();
▶ Try it Yourself
JAVASCRIPT
// fib-worker.js
const { parentPort } = require('worker_threads');

parentPort.on('message', ({ n }) => {
  const result = fib(n);
  parentPort.postMessage(result);
});

function fib(n) {
  if (n <= 1) return n;
  let a = 0, b = 1;
  for (let i = 2; i <= n; i++) {
    [a, b] = [b, a + b];
  }
  return b;
}

▶ Example: piscina thread pool library

BASH
npm install piscina
JAVASCRIPT
const path = require('path');
const Piscina = require('piscina');

const pool = new Piscina({
  filename: path.resolve(__dirname, 'task.js'),
  maxThreads: 4
});

async function main() {
  const results = await Promise.all([
    pool.run({ x: 10, y: 20 }),
    pool.run({ x: 30, y: 40 }),
    pool.run({ x: 50, y: 60 })
  ]);

  console.log(results); // [30, 70, 110]
  await pool.destroy();
}

main();
JAVASCRIPT
// task.js
module.exports = ({ x, y }) => {
  return x + y;
};


12. Comprehensive Example: Image Processing Thread Pool

Using the knowledge covered earlier, build a complete image processing thread pool system: the main thread dispatches tasks, workers process thumbnails, and results are collected and aggregated.

JAVASCRIPT
// image-pool.js
const { Worker } = require('worker_threads');
const path = require('path');

class ImagePool {
  constructor(workerCount) {
    this.workers = [];
    this.queue = [];
    this.results = [];

    for (let i = 0; i < workerCount; i++) {
      const worker = new Worker(path.join(__dirname, 'image-processor.js'));
      worker.busy = false;

      worker.on('message', (msg) => {
        if (msg.type === 'result') {
          this.results.push(msg.data);
        }
        worker.busy = false;
        worker.currentResolve();
        this._dispatch();
      });

      worker.on('error', (err) => {
        worker.busy = false;
        if (worker.currentReject) {
          worker.currentReject(err);
        }
        this._dispatch();
      });

      this.workers.push(worker);
    }
  }

  process(fileList) {
    this.results = [];
    const promises = fileList.map((file) => this._enqueue(file));
    return Promise.all(promises).then(() => this.results);
  }

  _enqueue(file) {
    return new Promise((resolve, reject) => {
      this.queue.push({ file, resolve, reject });
      this._dispatch();
    });
  }

  _dispatch() {
    while (this.queue.length > 0) {
      const idle = this.workers.find((w) => !w.busy);
      if (!idle) break;
      const task = this.queue.shift();
      idle.busy = true;
      idle.currentResolve = task.resolve;
      idle.currentReject = task.reject;
      idle.postMessage({ file: task.file, sizes: [200, 400, 800] });
    }
  }

  destroy() {
    this.workers.forEach((w) => w.terminate());
  }
}

module.exports = ImagePool;
JAVASCRIPT
// image-processor.js
const { parentPort } = require('worker_threads');

parentPort.on('message', async ({ file, sizes }) => {
  const results = [];

  for (const size of sizes) {
    const thumb = await resize(file, size);
    results.push(thumb);
  }

  parentPort.postMessage({
    type: 'result',
    data: { file, thumbnails: results }
  });
});

async function resize(file, maxSize) {
  return new Promise((resolve) => {
    const duration = Math.random() * 200 + 50;
    setTimeout(() => {
      resolve(`${file}_${maxSize}px.jpg`);
    }, duration);
  });
}
JAVASCRIPT
// run.js
const ImagePool = require('./image-pool.js');

async function main() {
  const pool = new ImagePool(4);

  const files = Array.from({ length: 20 }, (_, i) =>
    `photo-${String(i + 1).padStart(3, '0')}.jpg`
  );

  const start = Date.now();
  const results = await pool.process(files);
  const elapsed = Date.now() - start;

  console.log(`Processed ${files.length} images in ${elapsed} ms`);
  console.log(`Generated ${results.reduce((sum, r) => sum + r.thumbnails.length, 0)} thumbnails`);

  pool.destroy();
}

main();
BASH
node run.js
TEXT 📖 Display only
Processed 20 images in 1234 ms
Generated 60 thumbnails

❓ FAQ

Q What is the difference between worker threads and child_process?
A Worker threads are created within the same process and can share memory (SharedArrayBuffer), resulting in low communication overhead; child_process creates separate processes with isolated memory and communicates via serialized IPC, which has higher overhead but is more secure.
Q Can a worker access variables in the main thread?
A No. Workers have their own execution context and cannot directly access variables in the main thread. You must use postMessage for message passing, pass values via workerData during initialization, or use SharedArrayBuffer to share memory.
Q When should you use worker threads?
A For CPU-intensive tasks such as image processing, encryption, compression and decompression, large-scale mathematical calculations, and audio and video transcoding. I/O-intensive tasks do not require worker threads; Node.js’s asynchronous I/O is already sufficiently efficient.
Q Is SharedArrayBuffer safe?
A SharedArrayBuffer does not provide any synchronization mechanisms on its own; concurrent reads and writes in a multithreaded environment may lead to race conditions. You must use atomic operations (such as add, load, store, and compareExchange) to ensure atomicity, or use locks to coordinate access.
Q Is there a significant overhead involved in creating a Worker?
A Creating a Worker requires initializing a V8 engine instance and an event loop, which takes about 30–50 ms—less than with child_process, but still not negligible. We recommend reusing Workers in a thread pool to avoid frequent creation and destruction.
Q Can Node.js modules such as fs and http be used within a Worker?
A Yes. Worker threads have a full Node.js runtime and support the vast majority of built-in modules. However, certain modules, such as cluster, can only be used in the main process.
Q What is the difference between “Transferable” and “structured cloning”?
A Structured cloning copies the data and is suitable for small datasets; “Transferable” transfers ownership to the target thread—it’s a zero-copy operation, but the original thread loses access—and is suitable for scenarios involving large ArrayBuffers and the like.

📖 Summary


📝 Exercises

  1. Complete all the code examples in this lesson and make sure each one runs correctly.
  2. Modify the comprehensive example and add your own extensions
  3. Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
  4. Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
  5. Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏