Topic 7: Multi-Processing in NodeJS
📖 6 min read · 🎯 advanced · 🧭 Prerequisites: reading-post-data, crud-operations
Why this matters
Here's the thing — Node.js runs on a single thread. That's usually fine, and it's actually one of its strengths for I/O work. But the moment you throw something CPU-heavy at it — image processing, data crunching, heavy computation — everything else in your app has to wait. Your server just... freezes for other users. If your machine has eight cores and you're only using one, you're leaving a lot of power on the table. That's exactly what child_process and cluster solve — they let Node.js break out of that single thread and actually use the full machine beneath it.
What You'll Learn
- Understand why Node.js is single-threaded and when multi-processing becomes necessary
- Spawn and communicate with child processes using the
child_processmodule (spawnandfork) - Create a load-balanced HTTP server across all CPU cores using the
clustermodule - Exchange messages between master and worker processes using IPC (Inter-Process Communication)
The Analogy
Think of a busy restaurant kitchen. A single chef (the main Node.js thread) is brilliant at taking orders and coordinating I/O — fetching ingredients, waiting for the oven — but if every dish requires an hour of hand-chopping, that chef grinds to a halt. Multi-processing is hiring a brigade: the head chef (master process) still manages the floor, but the sous-chefs (worker processes) each own a burner and handle the CPU-heavy prep in parallel. The dining room never waits for one chef to finish; orders are routed to whoever is free. When a sous-chef calls in sick (a worker crashes), the head chef immediately hires a replacement so service never stops.
Chapter 1: Introduction to Multi-Processing in Node.js
Node.js is single-threaded and uses an event-driven model optimized for I/O operations — reading files, querying databases, making HTTP requests. This model is highly efficient for I/O-bound work because the thread never blocks; it simply registers a callback and moves on.
The problem arises with CPU-intensive tasks: image resizing, cryptography, large dataset processing, or complex computation. These tasks keep the thread busy, blocking the event loop and making the application unresponsive to all other requests while that work runs.
Node.js addresses this with two built-in modules:
child_process— spawn entirely new OS processes (Node.js or otherwise) and communicate with themcluster— a higher-level abstraction that forks multiple Node.js worker processes that all share the same server port, enabling automatic load balancing
Chapter 2: Using the child_process Module
The child_process module lets you spawn new processes and execute shell commands or other Node.js scripts in parallel with your main process.
Spawning a Child Process
spawn launches a command in a new OS process and streams its stdout/stderr back to you asynchronously.
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
Expected output:
stdout: total 0
...
child process exited with code 0
Key points about spawn:
- Returns a
ChildProcessobject withstdout,stderr, andstdinstreams - Ideal when the child process produces a large output you want to stream
- Non-blocking — the parent event loop continues while the child runs
Forking a Child Process
fork is a specialised variant of spawn for creating new Node.js processes. It establishes a built-in IPC (message-passing) channel between parent and child, so the two processes can call .send() and listen for 'message' events directly.
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
Expected output:
Message from parent: { message: 'Hello from parent' }
Message from child: { reply: 'Hello from child' }
The IPC channel is serialized JSON under the hood, so you can pass plain objects back and forth without any additional serialization setup.
Chapter 3: Using the cluster Module
The cluster module is purpose-built for scaling HTTP servers. It lets a master process fork one worker per CPU core; all workers bind to the same port, and the OS (or Node's round-robin scheduler on Linux/macOS) distributes incoming connections across them.
Example: Load-Balanced HTTP Server
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 {
// Each worker shares the same TCP port
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 incoming requests are distributed across all CPU cores automatically.
How it works:
cluster.isMasteristrueonly in the original process;cluster.fork()creates a copy that runs the same file but withcluster.isMaster === falseos.cpus().lengthreturns the number of logical CPU cores so you fork exactly the right number of workers- The
'exit'handler callscluster.fork()again so a crashed worker is immediately replaced — the server never goes down
flowchart 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]
CLIENT[HTTP Client] -->|:8000| W1
CLIENT -->|:8000| W2
CLIENT -->|:8000| W3
CLIENT -->|:8000| W4
W1 -->|exit event| M
M -->|restart fork| W5[New Worker<br/>PID: 1005]
Chapter 4: Handling IPC (Inter-Process Communication)
Beyond load balancing, the master process often needs to send configuration or task data to workers, and workers need to report back results. The cluster module includes a built-in IPC channel just like fork does.
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 and set up two-way communication
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 loads its own logic
require('./worker.js');
}
Run it:
node app.js
Expected output:
Worker 12345 received message from master: { message: 'Hello from master' }
Master received message from worker 12345: { reply: 'Hello from worker 12345' }
...
IPC design notes:
- Messages are passed as serialized JSON — keep payloads small and serializable
- Each
workerobject in the master is anEventEmitter; attach.on('message', ...)per worker to distinguish sources - Workers use
process.send()andprocess.on('message', ...)— the same API asfork - IPC is best for control messages (config, signals, stats); avoid routing large data buffers through it
🧪 Try It Yourself
Task: Build a clustered HTTP server that tracks how many requests each worker has handled and reports the count back to the master via IPC every 5 seconds.
- Scaffold
app.jsusing the cluster example above - In the worker, keep a local
let count = 0and increment it on each request - Use
setInterval(5000 ms) inside the worker to callprocess.send({ pid: process.pid, count }) - In the master's
worker.on('message', ...)handler, log the report
Success criterion: Running node app.js and hitting http://localhost:8000/ several times should produce master log lines like:
Stats from worker 1234: { pid: 1234, count: 7 }
Stats from worker 1235: { pid: 1235, count: 4 }
Starter snippet for the worker section:
let count = 0;
http.createServer((req, res) => {
count++;
res.writeHead(200);
res.end(`Handled by worker ${process.pid}\n`);
}).listen(8000);
setInterval(() => {
process.send({ pid: process.pid, count });
}, 5000);
🔍 Checkpoint Quiz
Q1. Why is multi-processing useful in Node.js even though Node already handles concurrent I/O efficiently?
A) Because Node.js cannot handle more than one HTTP request at a time
B) Because CPU-intensive tasks block the event loop, stalling all other operations
C) Because the http module requires multiple processes to open port 80
D) Because JavaScript is not capable of asynchronous operations
Q2. Given this snippet, what is printed first?
const { fork } = require('child_process');
const child = fork('./child.js');
child.send({ msg: 'ping' });
child.on('message', (m) => console.log('Parent got:', m));
console.log('Parent sent message');
A) Parent got: ... then Parent sent message
B) Parent sent message — the child.on('message', ...) callback fires later, asynchronously
C) Nothing — fork blocks until the child exits
D) A syntax error because child.send must be awaited
Q3. What does cluster.fork() do differently from child_process.fork()?
A) It runs a Python script instead of a Node.js script
B) It creates a worker that shares the master's server port and is managed by the cluster module's lifecycle events
C) It is only available on Windows
D) It spawns a thread inside the same process, not a new OS process
Q4. You have an 8-core machine running a clustered Node.js HTTP server. One worker crashes. What happens next, given the cluster.on('exit', ...) handler shown in the lesson?
A) The master process also crashes
B) The remaining 7 workers continue serving requests, and cluster.fork() in the exit handler immediately spawns a replacement worker
C) All workers are killed and the server restarts from scratch
D) The crashed worker is automatically restarted by the OS without any Node.js involvement
A1. B — The event loop is single-threaded; a CPU-bound task (e.g., hashing a large file) occupies the thread entirely and blocks all incoming requests until it completes.
A2. B — console.log('Parent sent message') is synchronous and runs immediately. The 'message' event from the child fires later when the child process sends its response, which is asynchronous.
A3. B — cluster.fork() creates a new OS process that is registered with the cluster module, participates in the shared port/load-balancing scheme, and emits lifecycle events (exit, online) on the cluster object. child_process.fork() creates a standalone Node.js process with an IPC channel but no shared port management.
A4. B — The 'exit' listener fires on the master, logs the worker's PID, then calls cluster.fork() to bring the total worker count back to numCPUs. The remaining workers never stop serving during this time.
🪞 Recap
- Node.js is single-threaded by default;
child_processandclusterare the two built-in ways to leverage multiple CPU cores. spawnlaunches any OS command as a child process with streaming I/O;forkcreates a new Node.js process with a built-in IPC channel.- The
clustermodule forks one worker per CPU core, all sharing the same port, giving you horizontal scale on a single machine with minimal code. - IPC lets master and worker processes exchange JSON messages via
process.send()and.on('message', ...), enabling coordination, configuration, and health reporting. - Always attach a
cluster.on('exit', ...)handler that callscluster.fork()to auto-restart crashed workers and keep your server resilient.
📚 Further Reading
- Node.js
child_processdocs — the source of truth onspawn,fork,exec, andexecFile - Node.js
clusterdocs — full API reference for master/worker lifecycle, IPC, and scheduling policies - Node.js
osmodule docs — coversos.cpus()and other system-info helpers used in cluster setups - PM2 Process Manager — production-grade cluster management that wraps Node's cluster module with zero-downtime reloads
- ⬅️ Previous: CRUD Operations
- ➡️ Next: Adding Middleware