Topic 7: Multi-Processing in NodeJS
📖 6 min read · 🎯 intermediate · 🧭 Prerequisites: basics-picker-status-bar-async-storage, crud-operations
Why this matters
Here's the thing — Node.js is single-threaded by default. That's usually fine, but the moment your app hits something CPU-heavy — image processing, number crunching, a complex data task — everything else just waits. Your server freezes up, requests pile on, and users stare at a spinning loader. I've seen this trip up so many developers who built something that worked great in testing, then collapsed under real load. That's exactly why we're going into child_process and cluster today — two built-in Node.js modules that let you spread work across multiple CPU cores instead of grinding through it all on one.
What You'll Learn
- Why Node.js is single-threaded and when that becomes a problem
- How to spawn and fork child processes using the
child_processmodule - How to use the
clustermodule to distribute HTTP load across all CPU cores - How to pass messages between processes using Inter-Process Communication (IPC)
The Analogy
Think of a single-threaded Node.js server as a one-window post office — every customer queues behind the one clerk, no matter how many empty windows line the wall. Multi-processing is the manager finally staffing all those windows: each clerk (worker process) handles their own queue, and a dispatcher (the master process) routes incoming customers to whichever window is free. The post office handles far more mail per hour without anyone running faster — just more hands on deck.
Chapter 1: Introduction to Multi-Processing in Node.js
Node.js runs on a single thread and uses an event-driven model for handling I/O operations. This is perfectly efficient for I/O-bound work — reading files, querying databases, making network requests — because the event loop can juggle thousands of pending callbacks without blocking.
The problem surfaces with CPU-intensive tasks: image processing, cryptography, complex computations. These hog the single thread and block every other operation until they finish.
Node.js ships two built-in modules to escape this constraint:
child_process— spawn entirely separate OS processes (Node.js or otherwise) and coordinate with themcluster— a higher-level abstraction that forks multiple Node.js processes sharing the same server port, ideal for HTTP load balancing
Chapter 2: Using the child_process Module
The child_process module lets you spawn new processes and execute commands in parallel, keeping the parent process free.
Spawning a Child Process
spawn launches a shell command as a separate process and streams its stdout/stderr back to the parent.
app.js:
const { spawn } = require('child_process');
// Spawn a new process to run a shell command
const ls = spawn('ls', ['-lh', '/usr']);
// Listen for data output from the child process
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
// Listen for error output from the child process
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
// Listen for the child process to exit
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
Run it:
node app.js
Output:
stdout: total 0
...
child process exited with code 0
Forking a Child Process
fork is a specialised variant of spawn for creating a new Node.js process. Unlike spawn, forked processes can communicate with the parent via a built-in IPC message channel using process.send() and process.on('message', ...).
child.js:
process.on('message', (msg) => {
console.log('Message from parent:', msg);
process.send({ reply: 'Hello from child' });
});
app.js:
const { fork } = require('child_process');
// Fork a new Node.js process
const child = fork('./child.js');
// Send a message to the child process
child.send({ message: 'Hello from parent' });
// Listen for messages from the child process
child.on('message', (msg) => {
console.log('Message from child:', msg);
});
Run it:
node app.js
Output:
Message from parent: { message: 'Hello from parent' }
Message from child: { reply: 'Hello from child' }
Key differences between spawn and fork:
| Feature | spawn | fork |
|---|---|---|
| Launches | Any executable | Node.js scripts only |
| Communication | stdin/stdout streams | Built-in IPC channel |
| Use case | Shell commands, binaries | Node-to-Node coordination |
Chapter 3: Using the cluster Module
The cluster module sits one level above child_process. It lets you fork multiple worker processes that all share the same TCP port — the OS load-balances incoming connections across them automatically.
Example: Using Cluster for Load Balancing
app.js:
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
console.log(`Master ${process.pid} is running`);
// Fork one worker per CPU core
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // Restart a new worker if one dies
});
} else {
// Workers share any TCP connection — here an HTTP server
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello, Vizag!\n');
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
Run it:
node app.js
Navigating to http://localhost:8000/ displays Hello, Vizag! and the load is distributed across all CPU cores automatically.
graph TD
M[Master Process<br/>PID: 1000] -->|fork| W1[Worker 1<br/>PID: 1001]
M -->|fork| W2[Worker 2<br/>PID: 1002]
M -->|fork| W3[Worker 3<br/>PID: 1003]
M -->|fork| W4[Worker 4<br/>PID: 1004]
C[Incoming HTTP Requests<br/>:8000] -->|OS load balances| W1
C -->|OS load balances| W2
C -->|OS load balances| W3
C -->|OS load balances| W4
W1 -->|exit event| M
M -->|restart| W1
How the master process works:
cluster.isMasteristrueonly in the original processcluster.fork()creates a copy of the current script running in worker mode- The
exitevent on the cluster fires when a worker dies — the master restarts it immediately, giving you automatic crash recovery
Chapter 4: Handling IPC (Inter-Process Communication)
The cluster module supports the same IPC message channel as fork — master and workers can exchange arbitrary JavaScript objects using worker.send() and process.send().
Example: IPC with Cluster
worker.js:
process.on('message', (msg) => {
console.log(`Worker ${process.pid} received message from master:`, msg);
process.send({ reply: `Hello from worker ${process.pid}` });
});
app.js:
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
// Fork workers
for (let i = 0; i < numCPUs; i++) {
const worker = cluster.fork();
// Send a message to the worker
worker.send({ message: 'Hello from master' });
// Listen for messages from the worker
worker.on('message', (msg) => {
console.log(
`Master received message from worker ${worker.process.pid}:`,
msg
);
});
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // Restart a new worker if one dies
});
} else {
// Worker process code
require('./worker.js');
}
Run it:
node app.js
Output:
Worker 12345 received message from master: { message: 'Hello from master' }
Master received message from worker 12345: { reply: 'Hello from worker 12345' }
...
IPC is useful for:
- Sending configuration updates from master to workers without restarting them
- Aggregating metrics — workers report stats, master collates them
- Coordinating graceful shutdowns across all workers
Chapter 5: Putting It Together
Here is how the two modules compare side by side so the class can pick the right tool:
| Scenario | Best Module |
|---|---|
| Run a shell script or binary | child_process → spawn |
| Offload a heavy Node.js task | child_process → fork |
| Scale an HTTP server across CPUs | cluster |
| Coordinate many workers with IPC | cluster + worker.send() |
A production pattern often combines both: cluster handles HTTP scaling, and individual workers use child_process.fork() to spin off CPU-intensive subtasks without blocking incoming requests.
🧪 Try It Yourself
Task: Build a clustered HTTP server that reports which worker PID served each request.
- Create
server.jswith the following starter code:
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
console.log(`Master PID: ${process.pid} — forking ${numCPUs} workers`);
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} exited — restarting`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Served by worker PID: ${process.pid}\n`);
}).listen(3000);
console.log(`Worker ${process.pid} listening on :3000`);
}
- Run it:
node server.js - In a second terminal, hit the server several times:
for i in {1..8}; do curl http://localhost:3000; done
Success criterion: You should see different PIDs appearing in the curl output — proof the OS is distributing requests across multiple worker processes.
🔍 Checkpoint Quiz
Q1. Why is Node.js single-threaded execution a problem for CPU-intensive tasks, but not for I/O-bound tasks?
A) Node.js can't handle I/O at all without multi-processing
B) CPU work blocks the event loop; I/O operations yield control while waiting
C) I/O tasks are faster so they don't need multi-threading
D) The cluster module is required for all I/O operations
Q2. Given this code, what will be printed first?
const { fork } = require('child_process');
const child = fork('./child.js');
child.send({ greeting: 'hi' });
child.on('message', (msg) => console.log('Parent got:', msg));
console.log('Message sent');
A) Parent got: ...
B) Message sent
C) Nothing — fork is asynchronous and never resolves
D) A syntax error because fork requires a callback
Q3. In a cluster-based server, cluster.isMaster is true in which process?
A) Every forked worker process
B) The process that called cluster.fork() — the original parent
C) Whichever worker receives the first HTTP request
D) The OS kernel process managing TCP
Q4. Your Node.js HTTP server handles image resizing on every request and is slow under load. You have an 8-core machine. How would you use the cluster module to improve throughput?
A1. B — The event loop can register an I/O operation and move on while the OS waits for the result. CPU work has no such pause point; it runs synchronously and blocks the event loop until it finishes.
A2. B — child.send() is non-blocking. console.log('Message sent') runs synchronously before any IPC round-trip completes.
A3. B — cluster.isMaster (or cluster.isPrimary in newer Node versions) is only true in the original process that bootstraps the cluster. All forked workers see it as false.
A4. Fork one worker per CPU core (os.cpus().length forks). Each worker runs the HTTP server on the same port; the OS load-balances connections. Image resizing in each worker now runs in parallel across all 8 cores instead of queuing on one thread. Add an exit handler to restart any worker that crashes.
🪞 Recap
- Node.js is single-threaded by default; CPU-intensive work blocks the event loop and must be offloaded to separate processes.
child_process.spawn()launches any shell command as a separate process with streaming stdout/stderr.child_process.fork()creates a new Node.js process with a built-in IPC message channel for parent-child communication.- The
clustermodule forks multiple worker processes that share a TCP port, letting the OS load-balance HTTP requests across all CPU cores. - IPC with
clusterallows the master to send configuration to workers and workers to report data back, all without restarting.
📚 Further Reading
- Node.js
child_processdocs — the source of truth onspawn,fork,exec, andexecFile - Node.js
clusterdocs — full API reference includingcluster.isPrimary(the modern alias forisMaster) - PM2 Process Manager — production-grade cluster management and zero-downtime reloads built on top of these primitives
- ⬅️ Previous: CRUD Operations
- ➡️ Next: React Lists