Node.js Multi-process
We all know that Node.js runs in single-threaded mode, but it uses event-driven processing to handle concurrency, which helps us create multiple child processes on multi-core CPU systems, thereby improving performance.
Each child process always has three stream objects: child.stdin, child.stdout, and child.stderr. They may share the parent process's stdio streams, or they can be independent stream objects that are piped.
Node provides the child_process and cluster modules to create child processes.
child_processModule
exec- child_process.exec uses a child process to execute a command, buffers the child process's output, and returns the child process's output in the form of callback function parameters.
execFile- Directly executes an executable file, slightly safer.
spawn- child_process.spawn creates a new process with specified command-line arguments.
fork- child_process.fork is a special form of spawn() for running modules in a child process, e.g., fork('./son.js') is equivalent to spawn('node', ['./son.js']). Unlike the spawn method, fork creates a communication channel between the parent process and the child process for inter-process communication.
clusterModule
- On one portMultiple worker processesShared listening, realizingone process per coreHTTP service.
- Suitable for horizontally filling up multiple cores with web services.
- Note: The official recommendation for new projects is to carefully evaluate (e.g., directly using a reverse proxy + multiple processes, or
worker_threadsfor CPU-intensive tasks), butclusterit remains widely usable.
exec() method
child_process.exec uses a child process to execute a command, buffers the child process's output, and returns the child process's output in the form of callback function parameters.
The syntax is as follows:
child_process.exec(command[, options], callback)
Parameters
The parameter descriptions are as follows:
command:String, the command to run, with parameters separated by spaces
options: Object, can be:
- cwd: String, the current working directory of the child process
- env: Object, environment variable key-value pairs
- encoding: String, character encoding (default: 'utf8')
- shell: String, the Shell to execute the command (default: in UNIX it is
/bin/sh, in Windows it iscmd.exe, the Shell should recognize-cswitches in UNIX, or/s /cin Windows. In Windows, command-line parsing should be compatible withcmd.exe) - timeout: Number, timeout time (default: 0)
- maxBuffer: Number, the maximum buffer (binary) allowed in stdout or stderr; if exceeded, the child process will be killed (default: 200*1024)
- killSignal: String, termination signal (default: 'SIGTERM')
- uid: Number, sets the user process ID
- gid: Number, sets the process group ID
callback :callback function, containing three parameters error, stdout, and stderr.
The exec() method returns the largest buffer, waits for the process to end, and returns the buffer contents all at once.
Example
Let's create two js files, support.js and master.js.
support.js file code:
master.js file code:
Execute the above code, the output result is:
$ node master.js 子进程已退出,退出码 0 stdout: 进程 1 执行。 stderr: 子进程已退出,退出码 0 stdout: 进程 0 执行。 stderr: 子进程已退出,退出码 0 stdout: 进程 2 执行。 stderr:
Analysis:
execwill start3 child processes, executing respectively:
node support.js 0node support.js 1node support.js 2
Each child process outputs immediately after running:
进程 0 执行。 进程 1 执行。 进程 2 执行。
This part is the child process'sstandard output (stdout), it will be buffered and returned together when the child process endsexecto the callback.
When the child process exits, it first triggers theexitevent, thenexecthe callback will execute (Note: Node.js documentation states thatexitthe event occurs when the process ends, andexecthe callback isstdiotriggered after the stream is closed; usually exit prints before the callback).
The output order is indeterminate, because the 3 child processes execute in parallel, and which one finishes first depends on the operating system scheduler.
spawn() method
child_process.spawn creates a new process with specified command-line arguments, the syntax is as follows:
child_process.spawn(command[, args][, options])
Parameters
The parameter descriptions are as follows:
command:The command to run
args:Array of string arguments
options Object
- cwd String, the current working directory of the child process
- env Object, environment variable key-value pairs
- stdio Array|String, the child process's stdio configuration
- detached Boolean, this child process will become the leader of a process group
- uid Number, sets the user process ID
- gid Number, sets the process group ID
The spawn() method returns streams (stdout & stderr), used when the process returns a large amount of data. As soon as the process starts executing, spawn() begins receiving responses.
Example
Let's create two js files, support.js and master.js.
support.js file code:
master.js file code:
Execute the above code, the output result is:
$ node master.js stdout: 进程 0 执行。 子进程已退出,退出码 0 stdout: 进程 1 执行。 子进程已退出,退出码 0 stdout: 进程 2 执行。 子进程已退出,退出码 0
fork method
child_process.fork is a special form of the spawn() method, used to create processes, the syntax is as follows:
child_process.fork(modulePath[, args][, options])
Parameters
The parameter descriptions are as follows:
modulePath: String, the module to run in the child process
args: Array of string arguments
options:Object
- cwd String, the current working directory of the child process
- env Object, environment variable key-value pairs
- execPath String, the executable file used to create the child process
- execArgv Array, the string argument array of the child process's executable file (default: process.execArgv)
- silent Boolean, if it is
true, the child process'sstdin,stdoutandstderrwill be associated with the parent process, otherwise, they will be inherited from the parent process. (default:false) - uid Number, sets the user process ID
- gid Number, sets the process group ID
The returned object, in addition to all the methods of a ChildProcess instance, also has a built-in communication channel.
Example
Let's create two js files, support.js and master.js.
support.js file code:
master.js file code:
Run the above code, the output is:
$ node master.js 进程 0 执行。 子进程已退出,退出码 0 进程 1 执行。 子进程已退出,退出码 0 进程 2 执行。 子进程已退出,退出码 0
cluster: multi-process HTTP service on one port
Minimal usable multi-core HTTP service
Example
import cluster from 'node:cluster';
import os from 'node:os';
import http from 'node:http';
import { pbkdf2Sync } from 'node:crypto';
if (cluster.isPrimary) {
const cpuCount = Math.max(1, os.cpus().length);
console.log(`Main process ${process.pid}, starting ${cpuCount}worker processes`);
for (let i = 0; i < cpuCount; i++) cluster.fork();
cluster.on('exit', (worker, code) => {
console.warn(`Worker process ${worker.process.pid}exited (code=${code}), restarting...`);
cluster.fork();
});
} else {
const server = http.createServer((req, res) => {
// Simulate CPU-intensive computation
pbkdf2Sync('password', 'salt', 100_000, 64, 'sha512');
res.writeHead(200, {'content-type':'text/plain; charset=utf-8'});
res.end(`Handled by worker ${process.pid}\n`);
});
server.listen(3000, () => {
console.log(`Worker process ${process.pid}listening3000`);
});
}
Run:node server-cluster.js, multiple requestshttp://localhost:3000will see differentPIDresponses.
Note:Multiple online instances usually have a reverse proxy/load balancer (Nginx, Envoy, K8s Service) in front, or use cluster to aggregate on one port.
WebSocket/sticky sessions require "sticky session" (same client lands on same worker). You can use source address hashing in L4 load balancing, or implement stickiness yourself at the application layer (e.g., dispatch by req.socket.remoteAddress).
Graceful restart (zero-downtime approach)
Approach:Main process receives reload signal → fork new worker first and wait for its listening → then disconnect old worker, wait for requests to finish before exiting.
Example
process.on('SIGUSR2', async () => {
console.log('Received SIGUSR2, starting graceful restart');
const workers = Object.values(cluster.workers ?? {});
// 1) Start new ones
const fresh = cluster.fork();
await new Promise(r => fresh.once('listening', r));
// 2) Shut down old ones one by one
for (const w of workers) {
w?.disconnect();
// Force kill if timeout hasn't exited
setTimeout(() => w?.process.kill('SIGKILL'), 5000);
}
});
Windows doesn't have SIGUSR2, you can use HTTP / RPC management interface to trigger reload.
Build your own "process pool": control concurrency and reuse child processes
When you have a large number of independent CPU tasks (like batch compression, encryption, crawler parsing), frequent fork/spawn costs are high; at this time you need Process Pool to reuse a few child processes, like a "thread pool" to limit concurrency.
File:pool/worker.js
Example
process.on('message', async (msg) => {
if (msg.type === 'task') {
const { id, payload } = msg;
// Simulate heavy task (Fibonacci)
const fib = (n) => (n <= 1 ? n : fib(n-1) + fib(n-2));
const result = fib(payload.n);
process.send({ type: 'done', id, result });
}
});
File:pool/index.js
Example
import { fork } from 'node:child_process';
import os from 'node:os';
export class ProcessPool {
constructor({ file, size = Math.max(1, os.cpus().length - 1) } = {}) {
this.file = file;
this.size = size;
this.idle = [];
this.busy = new Map(); // worker -> taskId
this.queue = [];
for (let i = 0; i < size; i++) this._spawn();
}
_spawn() {
const w = fork(this.file);
w.on('message', (m) => {
if (m?.type === 'done') {
const cb = this.callbacks.get(m.id);
if (cb) cb.resolve(m.result);
this.callbacks.delete(m.id);
this._markIdle(w);
this._drain();
}
});
w.on('exit', () => {
// Auto-fill the pool
this.busy.delete(w);
const idx = this.idle.indexOf(w);
if (idx >= 0) this.idle.splice(idx, 1);
this._spawn();
});
if (!this.callbacks) this.callbacks = new Map();
this.idle.push(w);
}
_markIdle(w) {
this.busy.delete(w);
if (!this.idle.includes(w)) this.idle.push(w);
}
_acquire() {
return this.idle.length ? this.idle.shift() : null;
}
_drain() {
while (this.queue.length && this.idle.length) {
const { id, payload, resolve, reject } = this.queue.shift();
const w = this._acquire();
this.busy.set(w, id);
this.callbacks.set(id, { resolve, reject });
w.send({ type: 'task', id, payload });
}
}
runTask(payload) {
const id = Math.random().toString(36).slice(2);
return new Promise((resolve, reject) => {
this.queue.push({ id, payload, resolve, reject });
this._drain();
});
}
close() {
for (const w of this.idle) w.kill();
for (const w of this.busy.keys()) w.kill();
}
}
File:pool/demo.js
Example
import { ProcessPool } from './index.js';
const pool = new ProcessPool({ file: './pool/worker.js', size: 4 });
const tasks = Array.from({ length: 10 }, (_, i) => pool.runTask({ n: 35 + (i % 3) }));
const t0 = Date.now();
const results = await Promise.all(tasks);
console.log('Result:', results);
console.log('Elapsed time(ms):', Date.now() - t0);
await new Promise(r => setTimeout(r, 100)); // Wait for messages to flush
pool.close();
Explanation:
- Wrap "tasks" as messages, the process pool is responsible for assigning and reusing workers.
- In real business, you canparsing/transcoding/compressionencapsulate into
worker.js. - NoteTask timeout、retry、idempotencyandbackpressure(queue length limit).
Practical example: HTTP interface offloads heavy tasks to a process pool
Example
import http from 'node:http';
import { ProcessPool } from './pool/index.js';
const pool = new ProcessPool({ file: './pool/worker.js', size: 4 });
const server = http.createServer(async (req, res) => {
if (req.url?.startsWith('/fib?')) {
const url = new URL(req.url, 'http://localhost');
const n = Number(url.searchParams.get('n') || 35);
try {
const result = await pool.runTask({ n });
res.writeHead(200, {'content-type': 'application/json'});
res.end(JSON.stringify({ pid: process.pid, n, result }));
} catch (e) {
res.writeHead(500); res.end('error');
}
} else {
res.writeHead(404).end('Not Found');
}
});
server.listen(3000, () => console.log('http://localhost:3000'));
Visit: /fib?n=38etc., the main process is not blocked, the task is computed in the child process.
Other extensions