Topic 8 of 18 · Node Expert

Topic 7 : Multi-Processing in NodeJS

Lesson TL;DRTopic 7: MultiProcessing in NodeJS 📖 6 min read · 🎯 advanced · 🧭 Prerequisites: readingpostdata, crudoperations Why this matters Here's the thing — Node.js runs on a single thread. That's usually f...
6 min read·advanced·node-js · multi-processing · child-process · cluster

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_process module (spawn and fork)
  • Create a load-balanced HTTP server across all CPU cores using the cluster module
  • 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 them
  • cluster — 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 ChildProcess object with stdout, stderr, and stdin streams
  • 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.isMaster is true only in the original process; cluster.fork() creates a copy that runs the same file but with cluster.isMaster === false
  • os.cpus().length returns the number of logical CPU cores so you fork exactly the right number of workers
  • The 'exit' handler calls cluster.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 worker object in the master is an EventEmitter; attach .on('message', ...) per worker to distinguish sources
  • Workers use process.send() and process.on('message', ...) — the same API as fork
  • 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.

  1. Scaffold app.js using the cluster example above
  2. In the worker, keep a local let count = 0 and increment it on each request
  3. Use setInterval (5000 ms) inside the worker to call process.send({ pid: process.pid, count })
  4. 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_process and cluster are the two built-in ways to leverage multiple CPU cores.
  • spawn launches any OS command as a child process with streaming I/O; fork creates a new Node.js process with a built-in IPC channel.
  • The cluster module 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 calls cluster.fork() to auto-restart crashed workers and keep your server resilient.

📚 Further Reading

Like this topic? It’s one of 18 in Node Expert.

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