Topic 23 of 56 · Full Stack Advanced

Topic 7 : Multi-Processing in NodeJS

Lesson TL;DRTopic 7: MultiProcessing in NodeJS 📖 6 min read · 🎯 intermediate · 🧭 Prerequisites: basicspickerstatusbarasyncstorage, crudoperations Why this matters Here's the thing — Node.js is singlethreaded b...
6 min read·intermediate·nodejs · multi-processing · child-process · cluster

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_process module
  • How to use the cluster module 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 them
  • cluster — 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:

Featurespawnfork
LaunchesAny executableNode.js scripts only
Communicationstdin/stdout streamsBuilt-in IPC channel
Use caseShell commands, binariesNode-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.isMaster is true only in the original process
  • cluster.fork() creates a copy of the current script running in worker mode
  • The exit event 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:

ScenarioBest Module
Run a shell script or binarychild_processspawn
Offload a heavy Node.js taskchild_processfork
Scale an HTTP server across CPUscluster
Coordinate many workers with IPCcluster + 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.

  1. Create server.js with 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`);
}
  1. Run it: node server.js
  2. 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 cluster module forks multiple worker processes that share a TCP port, letting the OS load-balance HTTP requests across all CPU cores.
  • IPC with cluster allows the master to send configuration to workers and workers to report data back, all without restarting.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.