Open-source project
josdejong/workerpool avatar
josdejong/workerpool

workerpool: a thread pool that works the same in Node and the browser

Offload tasks to a pool of workers on node.js and in the browser

2,310 stars164 forksJavaScriptApache-2.0

At a glance

What is it?
An Apache-2.0 library that offloads CPU-bound work to workers through a promise-based proxy, hiding the differences between Web Workers, worker_threads and child processes.
Who is it for?
workerpool earns its place by making one API span three different worker mechanisms, Web Workers in the browser, worker_threads and child processes in Node, without asking you to branch on the environment. The thread pool pattern underneath is unremarkable, but the ergonomics around it are not: a promise-based proxy that makes a remote worker look like a local object, plus cancellation, timeouts and recovery from a crashed worker.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 23 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One API over three different worker mechanisms

workerpool offers a pool of workers for dynamically offloading computations and for managing a pool of dedicated workers, and implements the thread pool pattern: a pool of workers, a queue of new tasks, one task per worker at a time, and a worker that picks up the next task when it finishes.

The distinguishing part is that the same code path works in Node and in the browser, across genuinely different mechanisms. In a browser you have Web Workers. In Node you have both child processes and worker_threads. The README frames the result as an architecture achieving concurrency through isolated processes and message passing, which is the correct characterisation.

What you get in exchange is that you never write the environment branch. That is a real saving, and it is the reason to pick this over calling the platform APIs directly when your code has to run in both places.

The scale: 2,310 stars, 164 forks, 44 open issues, Apache-2.0 licensed, not archived, last push on 2026-09-13. The `package.json` puts the version at 10.0.3, which for a library with this much history in JavaScript is a sign of stability rather than churn. The feature list claims a size of 9 kB minified and gzipped, and given the API surface that is a genuinely small dependency.

The problem it solves is stated better than most

The Why section of the README opens with a quote that explains the whole category of problem: in Node.js everything runs in parallel except your code. All I/O is non-blocking, while all non-I/O code is blocking.

The consequences are spelled out concretely. In a browser, CPU-intensive work stops the page reacting to user events, so the browser hangs. On a Node server, a single heavy request means no other request gets a response, because they are all sharing one event loop.

That framing is more precise than the usual vague performance talk, because it identifies the actual constraint. I/O parallelism on Node is not the issue and never has been. The issue is the single event loop that everything non-blocking shares, and any CPU-bound work on it delays every other task including the ones handling incoming requests.

So the recommendation is to split the application into separate decoupled parts that can run independently, which in JavaScript means workers. Once you accept that, the design question becomes which abstraction to use, and workerpool's answer is to hide the platform differences.

Two ways to use it, dynamic functions and dedicated workers

The first mode offloads a function you already have, along with its arguments, to a worker. You create a pool with no script argument and call `exec` with the function reference:

js
const workerpool = require('workerpool');
const pool = workerpool.pool();

function add(a, b) {
  return a + b;
}

pool
  .exec(add, [3, 4])
  .then(function (result) {
    console.log('result', result); // outputs 7
  })
  .catch(function (err) {
    console.error(err);
  })
  .then(function () {
    pool.terminate(); // terminate all workers when done
  });

The critical constraint is stated immediately afterwards: both the function and the arguments must be static and stringifiable, because they are sent to the worker in serialized form. For large functions or large arguments the serialization overhead can be significant. That is the trade, and it is the one thing to check before offloading anything data-heavy.

The second mode starts a dedicated worker from a separate script that registers its public functions, and you then call them by name. The README shows both a direct `exec` form and a proxy form, and the proxy is the more interesting of the two because it changes the shape of the calling code entirely.

The proxy makes a remote worker look like a local object

A dedicated worker is registered with `workerpool.worker`, passing an object whose properties are the functions it will expose:

js
const workerpool = require('workerpool');

// a deliberately inefficient implementation of the fibonacci sequence
function fibonacci(n) {
  if (n < 2) return n;
  return fibonacci(n - 2) + fibonacci(n - 1);
}

// create a worker and register public functions
workerpool.worker({
  fibonacci: fibonacci,
});

The main application points at that script and gets a worker object that responds to method calls, returning promises:

js
const pool = workerpool.pool(__dirname + '/myWorker.js');

pool
  .proxy()
  .then(function (worker) {
    return worker.fibonacci(10);
  })
  .then(function (result) {
    console.log('Result: ' + result); // outputs 55
  })

Calling `worker.fibonacci(10)` rather than `pool.exec('fibonacci', [10])` is a genuine improvement in readability, and it scales to a worker with a dozen methods rather than string keys scattered through your call sites.

One Node-specific note: when you pass a script, the README says it must be an absolute file path, since the path is not resolved relative to the calling module.

The feature list also covers cancelling running tasks, setting a timeout on tasks, handling crashed workers so the pool replaces them rather than dying, and supporting transferable objects on web workers and worker_threads.

Loading it in each environment, and the webpack caveat

Loading is a one-liner per environment. In Node, both the main application and the worker itself use `require('workerpool')`. In a browser page you include the script tag, and inside a web worker you use `importScripts('workerpool.js')`.

There is one documented rough edge: setting up workerpool with React or webpack 5 requires additional configuration, with details in the webpack5 section linked from the README. That is the expected consequence of shipping a single bundle that has to work as both an application script and a worker script, and it is better that the project names it than that you discover it.

The examples directory is a good map of the feature surface: `offloadFunctions.js`, `dedicatedWorker.js`, `proxy.js`, `priorityQueue.js`, `transferableObjects.js`, `async.js`, `abort.js`, `cleanup.js`, `consoleCapture.js` and `dynamicOptions.js`, plus browser-specific directories and bundler integration examples for webpack 5, esbuild and Vite. Naming a directory for each capability is more informative than a single running example.

Build tooling is rollup, with `src/index.js` as the main entry and `dist/workerpool.js` as the browser bundle, and tests run under mocha with c8 for coverage and TypeScript declaration files built by `tsc`.

When a worker pool is the wrong answer

The honest counterpoint is that a worker pool is not free, and the README's own constraint tells you when it is not worth it. Both the function and its arguments must be serialized to reach the worker, so a task with a small computation and a large payload can easily cost more in copying than it saves. Before offloading something, it is worth measuring whether the payload or the computation dominates.

Worker startup is also real cost, which is the entire reason the pool exists: workers are created once and reused across queued tasks. A single-shot offload of a function that takes microseconds is a pessimisation.

Beyond that, there is a question of whether the work belongs off the main thread at all. On the server, the usual answer is that blocking work belongs on a separate process or an external service rather than a thread in the same server, and Node's own documentation points at both child processes and worker_threads for different reasons. On the client, if the work is genuinely small, yielding to the event loop or chunking it is often simpler than introducing a worker and its build configuration.

What workerpool removes is the boilerplate for the cases where a worker is clearly right: pool sizing, queueing, cleanup on exit, and behaving the same in both environments.

Editorial conclusion

workerpool earns its place by making one API span three different worker mechanisms, Web Workers in the browser, worker_threads and child processes in Node, without asking you to branch on the environment. The thread pool pattern underneath is unremarkable, but the ergonomics around it are not: a promise-based proxy that makes a remote worker look like a local object, plus cancellation, timeouts and recovery from a crashed worker. The cost is the one the README names plainly, functions and arguments must be static and stringifiable because they are serialized to the worker, so sending large payloads can cost more than the computation you are offloading. At 9 kB minified and gzipped, and with a proxy that removes the callback bookkeeping, this is a good default for CPU work in JavaScript. Start with `pool.exec` on a small function and measure the serialization overhead before committing to it.

Frequently asked questions

What is a worker pool?

It is a fixed collection of workers and a queue of tasks. Each worker takes one task at a time from the queue and picks up the next when it finishes, so the number of workers stays bounded while the number of tasks can be unlimited. The pattern exists to keep a bounded resource, your CPU cores, busy without creating an unbounded number of threads.

What is a worker thread?

A separate JavaScript execution context inside a Node process with its own event loop and memory, so CPU-bound work does not block the main one. It is the Node mechanism workerpool uses for dedicated workers. The other Node option is child processes, which are more isolated but heavier to start and to pass data between.

Does workerpool work in both Node and the browser?

Yes, and that is its main design point. The same pool API covers Web Workers in the browser, worker_threads and child processes in Node. Loading differs per environment: `require` in Node, a script tag in a browser page, and `importScripts` inside a web worker.

What are the limits of offloading a function to a worker?

Both the function and its arguments must be static and stringifiable, because they are serialized to reach the worker. For large functions or arguments the copying overhead can exceed the work saved, so it is worth measuring before offloading data-heavy work. The pool also carries worker startup cost, which is why reuse matters.

Official sources

  1. Issues
  2. josdejong/workerpool on GitHub
  3. License: Apache-2.0
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/josdejong-workerpool.svg)](https://hysenlabs.com/projects/josdejong-workerpool)