Skip to main content

Worker Threads & Child Processes

Node.js is single-threaded, but that does not mean you are limited to one CPU core. Worker Threads and Child Processes let you leverage multi-core systems for CPU-intensive operations. To understand why this matters, think of the Node.js event loop as a single chef in a kitchen. That chef can handle hundreds of orders by efficiently switching between tasks while things cook in the oven (I/O operations). But if one order requires the chef to hand-knead dough for 10 minutes straight (CPU-intensive work), every other order just waits. Worker threads are like hiring additional chefs for the heavy prep work, so the main chef stays free to handle incoming orders.

The Problem: Blocking the Event Loop

CPU-intensive operations block the event loop, making your server unresponsive to ALL requests. This is the #1 performance killer in Node.js applications.

Solutions Overview

Worker Threads

Worker Threads run JavaScript in parallel threads within the same process, sharing memory when needed. Unlike child processes, they do not spawn a separate OS process, so they have lower startup overhead and can share data directly via SharedArrayBuffer.

Basic Usage

Worker Pool

Creating a new worker for every request is expensive — each worker spins up a new V8 isolate, which takes milliseconds and memory. A worker pool pre-creates a fixed number of workers at startup and reuses them for incoming tasks. It is the same principle as database connection pooling: amortize the creation cost across many operations. When all workers are busy, new tasks queue up and wait for the next available worker. This also naturally applies backpressure — if tasks arrive faster than workers can process them, the queue grows, which you can monitor and use to trigger alerts or scaling.

SharedArrayBuffer for Shared Memory

Normal message passing between workers involves copying data — the main thread serializes the message, sends a copy, and the worker deserializes it. For large data or high-frequency communication, this copying overhead is significant. SharedArrayBuffer provides true shared memory: multiple threads read and write the same underlying bytes, with no copying. However, this introduces the classic problems of concurrent programming — race conditions and data corruption — so you must use Atomics operations (atomic read-modify-write) to safely access shared data.

Child Processes

While Worker Threads run JavaScript in parallel threads within a single process, Child Processes spawn entirely separate OS processes. They are heavier (each gets its own memory space and V8 instance), but they provide complete isolation and the ability to run any program — not just JavaScript.

exec - Run Shell Commands

spawn - Stream Output

The key difference between exec and spawn: exec buffers the entire output in memory and returns it all at once (limited to 200KB by default), while spawn streams output in real time. Use exec for short commands with small output; use spawn for long-running processes, large output, or when you need to process output incrementally.

fork - Run Node.js Scripts

fork is a specialized version of spawn designed specifically for running Node.js scripts. It automatically sets up an IPC (Inter-Process Communication) channel between parent and child, so they can exchange structured JavaScript objects via process.send() and process.on('message') — no manual serialization needed.

Real-World Use Cases

Image Processing with Workers

Image resizing and format conversion are classic CPU-bound tasks — the sharp library does pixel-level transformations that saturate a CPU core. Without workers, processing a single large image could block your server for 500ms+. With a worker pool, the main thread stays responsive while workers process images in parallel.

PDF Generation

Video Transcoding with FFmpeg

Video transcoding is a perfect use case for child processes: FFmpeg is an external C program (not JavaScript), it produces streaming progress output on stderr, and it can run for minutes on large files. spawn is the right choice here because it streams output incrementally — exec would buffer the entire stderr log in memory.

Best Practices

  1. Use Worker Pools - Don’t create new workers per request
  2. Set appropriate pool size - Usually number of CPU cores
  3. Handle errors properly - Workers can crash
  4. Clean up on shutdown - Terminate workers gracefully
  5. Don’t overuse workers - Only for CPU-intensive tasks
  6. Consider message serialization - Large data transfers have overhead
  7. Use SharedArrayBuffer - For shared state between threads

When to Use What

Summary

  • Worker Threads run JavaScript in parallel without blocking
  • Worker Pools reuse threads for better performance
  • SharedArrayBuffer enables shared memory between threads
  • Child Processes run external programs or Node.js scripts
  • Use exec for simple commands, spawn for streaming output
  • Use fork for Node.js scripts with IPC
  • Always handle errors and cleanup properly
  • Match pool size to CPU cores for optimal performance