background grid

Understanding Worker Pools in Node.js

Roopsagar K

Full-stack Developer | Freelancer | Building AI-powered SaaS Solutions

Sep 16, 2025 | 4 min read

GitHub iconLinkedIn iconTwitter icon
Understanding Worker Pools in Node.js

Table of Contents

Understanding Worker Pools in Node.js

When working with Node.js, one of its biggest strengths is the event-driven, non-blocking I/O model. This makes Node.js fantastic for handling lots of concurrent network requests. However, things get tricky when we deal with CPU-intensive tasks (like image processing, cryptography, or video encoding). These can block the event loop and slow down the entire application.

This is where worker threads and worker pools come in.

What Are Worker Threads?

By default, Node.js runs in a single thread (the main event loop). To run CPU-heavy tasks in parallel without blocking the main loop, Node.js introduced the worker_threads module.

  • A worker thread runs JavaScript in a separate thread.

  • You can communicate with workers via message passing.

  • Perfect for offloading heavy computations.

Example worker:

// worker.js
const { parentPort } = require("worker_threads");

parentPort.on("message", (job) => {
   // simulate heavy work
   const result = job.data * 2;
   parentPort.postMessage({ id: job.id, result });

});

The Need for a Worker Pool

If you spawn a new worker for every task, it quickly becomes inefficient:

  • Worker startup time is expensive.

  • Too many workers can overwhelm the CPU.

Instead, we use a worker pool:

  • A fixed number of workers (e.g. 4).

  • A job queue where tasks wait until a worker is free.

  • Tasks are assigned to workers as they become available.

This way, you maximize concurrency without overwhelming resources.

Manual Worker Pool Implementation

Here’s a simple worker pool using worker_threads:

// pool.js
const { Worker } = require("worker_threads");

class WorkerPool {
   constructor(workerPath, maxWorkers) {
   this.workerPath = workerPath;
   this.maxWorkers = maxWorkers;
   this.workers = [];
   this.idleWorkers = [];
   this.queue = [];
   this.jobId = 0;

   for (let i = 0; i < maxWorkers; i++) {
     this.addNewWorker();
   }
  }

  addNewWorker() {
    const worker = new Worker(this.workerPath);
    worker.on("message", (msg) => {
      const { resolve } = worker.currentJob;
      resolve(msg);
      worker.currentJob = null;
      this.idleWorkers.push(worker);
      this.runNext();
    });
   this.idleWorkers.push(worker);
   this.workers.push(worker);
  }

 runNext() {
   if (this.queue.length === 0 || this.idleWorkers.length === 0) return;
   const { job, resolve, reject } = this.queue.shift();
   const worker = this.idleWorkers.pop();
   worker.currentJob = { resolve, reject };
   worker.postMessage(job);
}
runJob(data) {
   return new Promise((resolve, reject) => {
   const job = { id: ++this.jobId, data };
   this.queue.push({ job, resolve, reject });
   this.runNext();
  });
}

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

module.exports = WorkerPool;

Using it:

const path = require("path");
const WorkerPool = require("./pool");

(async () => {
  const pool = new WorkerPool(path.resolve(__dirname, "worker.js"), 3);

  const jobs = [1, 2, 3, 4, 5, 6].map((num) => pool.runJob(num));
  const results = await Promise.all(jobs);

  console.log("Results:", results);
  pool.close();
})();

Supporting Different Job Types

You can also make workers handle different types of jobs:

// worker.js
const { parentPort } = require("worker_threads");

parentPort.on("message", (job) => {
  let result;
  switch (job.type) {
    case "double":
      result = job.payload * 2;
      break;
    case "square":
      result = job.payload ** 2;
      break;
    default:
      result = "Unknown job type";
  }
  parentPort.postMessage({ id: job.id, result });
});

Now you can push different jobs to the same worker pool.

Using the workerpool Module

While writing your own worker pool is a good learning exercise, in production it’s better to use a library. workerpool makes things much easier:

Example: Crypto Worker

// cryptoWorker.js
const workerpool = require('workerpool');
const crypto = require('crypto');


function hashString(str) {
   return crypto.createHash('sha256').update(str).digest('hex');
}


workerpool.worker({ hashString });

Example: Main File

const workerpool = require('workerpool');
const pool = workerpool.pool(__dirname + '/cryptoWorker.js', { maxWorkers: 2 });

(async () => {
const jobs = [
  pool.exec('hashString', ['hello']),
  pool.exec('hashString', ['world']),
];
console.log(await Promise.all(jobs));
pool.terminate();
})();

With just a few lines, you get a managed worker pool with queueing and max worker limits.

Real-World Use Cases

  • Image processing (resizing, compression)

  • Video encoding

  • Cryptographic operations (hashing, encryption)

  • Machine learning inference

  • Data transformation

Key Takeaways

  • Use worker threads to offload CPU-heavy tasks in Node.js.

  • Always use a worker pool to avoid overhead and CPU thrashing.

  • For production, libraries like workerpool or BullMQ (for distributed queues) are recommended.

  • Worker pools let you scale Node.js apps beyond I/O, handling compute-intensive workloads efficiently.

AI Tools to Boost Your Job Search
Struggling with job applications? Resumetweaker helps you craft the perfect resume and cover letter, practice with an AI-powered interview, and even track job applications effortlessly. Let AI simplify your job search and give you an edge.