Async code doesn't help when work is genuinely CPU-bound, calculations that take real time on the CPU itself. This lesson covers Worker Threads, Node's solution for running heavy computation without blocking the main thread.
Why Worker Threads Exist
Async/await and promises solve I/O-bound waiting, they don't make CPU-bound work (image processing, complex calculations, large data transformations) run any faster or avoid blocking the main thread, because that work genuinely needs the CPU the whole time, there's no "waiting" to yield during.
The worker_threads module lets you run JavaScript on a genuinely separate thread, with its own V8 instance and event loop, communicating with the main thread via message passing. This keeps the main thread free to handle other requests while the worker crunches through CPU-heavy work.
// worker.js
import { parentPort, workerData } from 'node:worker_threads';
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
parentPort.postMessage(fibonacci(workerData.n));
// main.js
import { Worker } from 'node:worker_threads';
const worker = new Worker('./worker.js', { workerData: { n: 40 } });
worker.on('message', (result) => {
console.log('Fibonacci result:', result);
});
The heavy fibonacci(40) computation runs entirely on the worker thread; the main thread's event loop stays completely free the whole time.
Core Worker Threads API
new Worker('./worker.js', { workerData })
worker.on('message', (data) => { ... })
worker.on('error', (err) => { ... })
worker.postMessage(data) // send data TO the worker
parentPort.postMessage(data) // send data FROM the worker
workerData passes initial data into a worker when it's created.
worker.postMessage()/parentPort.postMessage() send messages between threads after creation.
Workers have their own memory space, data is copied (or transferred) between threads, not shared by default.
Always attach an 'error' listener to a worker, unhandled worker errors can crash the whole process.
Worker Threads Cheatsheet
When to reach for Worker Threads versus other approaches.
Use Case
Best Fit
Waiting on a database/API
Regular async/await, no worker needed
Heavy CPU computation
Worker Threads
Image/video processing
Worker Threads (or a dedicated service)
Running multiple server instances
Clustering (covered next)
Shell commands/external programs
Child Process (covered next)
Reusing Workers With a Pool
Creating a new worker thread has real startup overhead. For applications that repeatedly need CPU-bound work done, a pool of pre-started, reusable workers (similar in spirit to a database connection pool) avoids paying that startup cost on every single task.
Libraries like piscina provide a ready-made, well-tested worker pool implementation.
A pool assigns incoming tasks to idle workers and queues tasks when all workers are busy.
Pool size is often set close to the number of available CPU cores (os.cpus().length).
Sharing Memory With SharedArrayBuffer
By default, data passed between the main thread and workers is copied. For performance-critical cases involving large binary data, SharedArrayBuffer allows multiple threads to read and write the same underlying memory directly, avoiding copy overhead, at the cost of needing careful synchronization to avoid race conditions.
Common Mistakes
Reaching for Worker Threads to solve I/O-bound waiting, which regular async/await already handles efficiently.
Creating a brand-new worker for every small task instead of reusing a pool.
Forgetting to handle the worker's 'error' event, risking an unhandled crash.
Assuming data is shared by reference between threads by default, it's copied unless using SharedArrayBuffer.
Key Takeaways
Worker Threads run genuinely CPU-bound JavaScript on separate threads, each with its own event loop.
They solve CPU-bound blocking, not I/O-bound waiting, which async/await already handles well.
Communication happens via message passing (postMessage), with data copied by default.
A worker pool avoids repeated worker-creation overhead for frequent CPU-heavy tasks.
Pro Tip
Before reaching for Worker Threads, confirm the bottleneck is actually CPU-bound (profile it first). Adding worker thread complexity to solve what's actually a slow database query or blocking I/O call adds overhead without fixing the real problem.
You now offload CPU-heavy work correctly. Next, learn Child Processes for running external programs and scripts from Node.js.