Skip to content

Node.js Scaling Application

While Node.js’s single-threaded, event-driven model excels at I/O concurrency, scaling applications to fully utilize multi-core processors requires additional strategies. Node.js provides built-in modules to manage multiple processes and threads, enabling true parallelism.

  • Clustering: Running multiple instances of your Node.js application (workers) that share the same server port. The built-in cluster module helps distribute incoming connections among workers.
  • Child Processes: Spawning separate Node.js processes using the child_process module. Useful for offloading specific tasks or running external commands.
  • Worker Threads: Using the worker_threads module (introduced in Node.js v10.5.0) to run JavaScript code in parallel within the same process, sharing memory more efficiently than child processes. Ideal for CPU-bound tasks.
  • External Tools: Using process managers like PM2 or system tools like systemd to manage multiple instances and restarts.
  • Load Balancers: Distributing traffic across multiple server instances running your Node.js application (often used in conjunction with clustering or process managers).

This chapter focuses on the child_process module. The cluster and worker_threads modules offer alternative approaches often better suited for scaling web servers and handling CPU-intensive work, respectively.

The child_process module allows you to spawn new processes. These child processes run independently of the main Node.js process and communicate with the parent via standard input/output streams (stdin, stdout, stderr) and an optional Inter-Process Communication (IPC) channel.

Key methods in child_process:

  • exec(): Spawns a shell (like bash or cmd.exe) and executes a command string within that shell. Buffers the entire output before returning it to the callback. Convenient for simple commands but potentially inefficient for large outputs.
  • spawn(): Spawns a new process directly using a specified command and arguments array. Provides access to stdin, stdout, and stderr as streams, making it suitable for large data or long-running processes.
  • fork(): A special case of spawn() specifically designed for creating new Node.js processes. It establishes an IPC channel between the parent and child, allowing them to send messages back and forth using .send() and .on('message').

child_process.exec(command[, options], callback) runs a command in a shell and buffers the output.

Signature:
child_process.exec(command[, options], callback)
  • command (String): The command to run, including space-separated arguments.
  • options (Object, Optional): Configuration options like cwd (current working directory), env (environment variables), timeout, maxBuffer (maximum size of stdout/stderr buffer, defaults to 1MB), killSignal, uid, gid.
  • callback (Function): Called when the process terminates or encounters an error. Receives (error, stdout, stderr) arguments. error is an Error object if the command failed, stdout is the standard output buffer, stderr is the standard error buffer.

Use exec when you need the full output at once and the output size is manageable.

Let’s create two files: supportScript.js (the child script) and masterExec.js (the parent).

File: supportScript.js

const processId = process.argv[2] || 'Unknown';
console.log(`Child Process ${processId} executed.`);
// Simulate some work
setTimeout(() => {
console.error(`Child Process ${processId} finishing.`);
}, 500);

File: masterExec.js

const { exec } = require('child_process');
console.log('Master process starting...');
for (let i = 0; i < 3; i++) {
// Note: Be cautious with user input in command strings due to shell injection risks.
// Consider using spawn or validating/escaping input rigorously.
const command = `node supportScript.js ${i}`;
console.log(`Executing: ${command}`);
const child = exec(command, (error, stdout, stderr) => {
console.log(`--- Callback for Child ${i} ---`);
if (error) {
console.error(`Exec error for child ${i}:`, error);
return;
}
if (stdout) {
console.log(`Child ${i} stdout:\n${stdout}`);
}
if (stderr) {
console.error(`Child ${i} stderr:\n${stderr}`);
}
});
child.on('exit', (code, signal) => {
console.log(`Child process ${i} exited with code ${code} and signal ${signal}`);
});
}
console.log('Master process finished initiating children.');

Run the master script:

$ node masterExec.js

Expected Output (order might vary due to asynchronicity):

Master process starting...
Executing: node supportScript.js 0
Executing: node supportScript.js 1
Executing: node supportScript.js 2
Master process finished initiating children.
--- Callback for Child X ---
Child X stdout:
Child Process X executed.
Child X stderr:
Child Process X finishing.
Child process X exited with code 0 and signal null
(Repeats similarly for other children)

child_process.spawn(command[, args][, options]) launches a new process directly without an intermediate shell. It provides access to I/O streams.

Signature:
child_process.spawn(command[, args][, options])
  • command (String): The command to execute (e.g., ‘node’, ‘ls’, ‘python’).
  • args (Array, Optional): List of string arguments for the command (e.g., [‘script.js’, ‘arg1’]).
  • options (Object, Optional): Similar options to exec, plus stdio (configures child’s stdin/stdout/stderr - e.g., ‘pipe’, ‘inherit’), detached (run independently of parent).

spawn returns a ChildProcess object with stdin, stdout, and stderr streams. Use spawn for large outputs, streaming data, or when you need finer control over the process and don’t need shell features.

Use the same supportScript.js. Create masterSpawn.js:

File: masterSpawn.js

const { spawn } = require('child_process');
console.log('Master process starting...');
for (let i = 0; i < 3; i++) {
const args = ['supportScript.js', i.toString()];
console.log(`Spawning: node ${args.join(' ')}`);
// Spawn the child process
const workerProcess = spawn('node', args);
// Listen for data on stdout stream
workerProcess.stdout.on('data', (data) => {
console.log(`Child ${i} stdout: ${data.toString().trim()}`);
});
// Listen for data on stderr stream
workerProcess.stderr.on('data', (data) => {
console.error(`Child ${i} stderr: ${data.toString().trim()}`);
});
// Listen for the 'close' event (when streams are closed)
workerProcess.on('close', (code) => {
console.log(`Child process ${i} exited with code ${code}`);
});
// Listen for potential spawn errors
workerProcess.on('error', (err) => {
console.error(`Failed to start child process ${i}:`, err);
});
}
console.log('Master process finished initiating children.');

Run the master script:

$ node masterSpawn.js

Expected Output (order might vary, stdout/stderr appear as data arrives):

Master process starting...
Spawning: node supportScript.js 0
Spawning: node supportScript.js 1
Spawning: node supportScript.js 2
Master process finished initiating children.
Child X stdout: Child Process X executed.
Child Y stdout: Child Process Y executed.
Child Z stdout: Child Process Z executed.
Child X stderr: Child Process X finishing.
Child Y stderr: Child Process Y finishing.
Child Z stderr: Child Process Z finishing.
Child process X exited with code 0
Child process Y exited with code 0
Child process Z exited with code 0

child_process.fork(modulePath[, args][, options]) is specifically for spawning new Node.js processes. It enables an IPC channel for message passing.

Signature:
child_process.fork(modulePath[, args][, options])
  • modulePath (String): The path to the Node.js module to run in the child.
  • args (Array, Optional): String arguments for the child script.
  • options (Object, Optional): Similar options to spawn, plus execPath (Node.js executable path), execArgv (Node.js runtime arguments for child), silent (pipe child’s stdio instead of inheriting).

fork returns a ChildProcess object with a .send(message) method and emits message events for communication.

Create supportFork.js (child) and masterFork.js (parent).

File: supportFork.js

const processId = process.argv[2] || 'Unknown';
console.log(`Child Process ${processId} started.`);
// Listen for messages from the parent
process.on('message', (msg) => {
console.log(`Child ${processId} received message:`, msg);
// Send a reply back to the parent
process.send({ reply: `Roger that from child ${processId}!` });
});
// Simulate some work and exit
setTimeout(() => {
console.log(`Child Process ${processId} exiting.`);
process.exit(0);
}, 1500);

File: masterFork.js

const { fork } = require('child_process');
console.log('Master process starting...');
for (let i = 0; i < 2; i++) { // Forking two children
const args = [i.toString()];
console.log(`Forking supportFork.js with args: ${args}`);
const worker_process = fork('supportFork.js', args);
// Send a message to the child process
worker_process.send({ message: `Hello from master to child ${i}` });
// Listen for messages from the child
worker_process.on('message', (msg) => {
console.log(`Master received message from child ${i}:`, msg);
});
worker_process.on('close', (code) => {
console.log(`Child process ${i} exited with code ${code}`);
});
worker_process.on('error', (err) => {
console.error(`Failed to fork child process ${i}:`, err);
});
}
console.log('Master process finished initiating children.');

Run the master script:

$ node masterFork.js

Expected Output (order/timing may vary):

Master process starting...
Forking supportFork.js with args: 0
Forking supportFork.js with args: 1
Master process finished initiating children.
Child Process 0 started.
Child Process 1 started.
Child 0 received message: { message: 'Hello from master to child 0' }
Master received message from child 0: { reply: 'Roger that from child 0!' }
Child 1 received message: { message: 'Hello from master to child 1' }
Master received message from child 1: { reply: 'Roger that from child 1!' }
Child Process 0 exiting.
Child process 0 exited with code 0
Child Process 1 exiting.
Child process 1 exited with code 1
  • exec: Simple shell commands, small output.
  • spawn: Fine control, streaming I/O, large data, no shell features needed.
  • fork: Spawning Node.js processes with built-in IPC.
  • worker_threads: CPU-bound tasks within the same process, shared memory (use with care).