Child Processes
Child process management patterns for spawning, communicating with, and controlling external processes
You are an expert in Node.js child process management for spawning, communicating with, and controlling external processes safely.
## Key Points
- **`spawn(command, args)`** — streams stdio, no shell by default, best for long-running processes or large output.
- **`exec(command)`** — runs in a shell, buffers all output, has a `maxBuffer` limit. Convenient for short commands.
- **`execFile(file, args)`** — like `exec` but bypasses the shell, safer for untrusted input.
- **`fork(modulePath)`** — spawns a new Node.js process with an IPC channel for `send()`/`on('message')` communication.
- Use `execFile` or `spawn` instead of `exec` when arguments come from user input — they avoid shell interpretation and injection attacks.
- Always handle the `'error'` event on spawned processes — it fires when the command is not found or cannot be executed.
- Set `timeout` and `maxBuffer` on `exec`/`execFile` to prevent runaway processes and memory exhaustion.
- Use `{ stdio: 'inherit' }` for CLI tools that need to pass through terminal formatting and interactivity.
- Forward `SIGTERM`/`SIGINT` to child processes in your shutdown handler to avoid orphaned processes.
- **Shell injection** — using `exec()` with unsanitized user input allows arbitrary command execution. Always use `execFile()` or `spawn()` with argument arrays.
- **Ignoring `maxBuffer`** — `exec()` defaults to 1 MB of buffered output; exceeding it kills the process silently. Increase the limit or use `spawn()` for large output.
- **Zombie processes** — not handling child exit events or not killing children on parent exit leaves orphaned processes consuming resources.skilldb get nodejs-patterns-skills/Child ProcessesFull skill: 198 linesChild Processes — Node.js Patterns
You are an expert in Node.js child process management for spawning, communicating with, and controlling external processes safely.
Core Philosophy
Overview
The node:child_process module lets Node.js applications run external commands, scripts, and executables. It provides four methods — spawn, exec, execFile, and fork — each suited to different use cases. Correct usage requires understanding stdio piping, signal handling, shell injection risks, and process lifecycle management.
Core Concepts
spawn vs exec vs execFile vs fork
spawn(command, args)— streams stdio, no shell by default, best for long-running processes or large output.exec(command)— runs in a shell, buffers all output, has amaxBufferlimit. Convenient for short commands.execFile(file, args)— likeexecbut bypasses the shell, safer for untrusted input.fork(modulePath)— spawns a new Node.js process with an IPC channel forsend()/on('message')communication.
stdio Configuration
The stdio option controls how the child's stdin, stdout, and stderr connect to the parent: 'pipe' (default), 'inherit', 'ignore', or a file descriptor.
IPC Channel
fork() and spawn() with { stdio: ['pipe','pipe','pipe','ipc'] } establish an Inter-Process Communication channel for exchanging serialized JavaScript objects.
Implementation Patterns
Running a command and capturing output
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');
const execFileAsync = promisify(execFile);
async function getGitHash() {
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD']);
return stdout.trim();
}
Streaming output from a long-running process
const { spawn } = require('node:child_process');
function tailLog(filePath, onLine) {
const child = spawn('tail', ['-f', filePath]);
let buffer = '';
child.stdout.on('data', (chunk) => {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop();
lines.forEach(onLine);
});
child.on('error', (err) => {
console.error('Failed to start tail:', err.message);
});
return () => child.kill('SIGTERM');
}
IPC communication with fork
// parent.js
const { fork } = require('node:child_process');
const worker = fork('./compute.js');
worker.send({ type: 'calculate', payload: { n: 1_000_000 } });
worker.on('message', (msg) => {
if (msg.type === 'result') {
console.log('Result:', msg.value);
worker.disconnect();
}
});
// compute.js
process.on('message', (msg) => {
if (msg.type === 'calculate') {
const result = heavyComputation(msg.payload.n);
process.send({ type: 'result', value: result });
}
});
Process pool with AbortController
const { execFile } = require('node:child_process');
async function runWithTimeout(cmd, args, timeoutMs) {
const ac = new AbortController();
try {
const { stdout } = await new Promise((resolve, reject) => {
const child = execFile(cmd, args, {
signal: ac.signal,
timeout: timeoutMs,
}, (err, stdout, stderr) => {
if (err) return reject(err);
resolve({ stdout, stderr });
});
});
return stdout;
} catch (err) {
if (err.killed) {
throw new Error(`Process ${cmd} timed out after ${timeoutMs}ms`);
}
throw err;
}
}
Safe command execution without shell injection
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');
const execFileAsync = promisify(execFile);
// DANGEROUS — shell injection vulnerability
// exec(`grep ${userInput} /var/log/app.log`);
// SAFE — arguments are passed as array, no shell involved
async function searchLogs(pattern) {
const { stdout } = await execFileAsync('grep', ['-F', pattern, '/var/log/app.log']);
return stdout.split('\n').filter(Boolean);
}
Graceful child process shutdown
const { spawn } = require('node:child_process');
function startService(command, args) {
const child = spawn(command, args, { stdio: 'inherit' });
async function shutdown() {
return new Promise((resolve) => {
child.once('exit', resolve);
child.kill('SIGTERM');
// Force kill after timeout
const timer = setTimeout(() => {
child.kill('SIGKILL');
}, 5000);
timer.unref();
});
}
process.on('SIGTERM', () => shutdown().then(() => process.exit(0)));
process.on('SIGINT', () => shutdown().then(() => process.exit(0)));
return { child, shutdown };
}
Best Practices
- Use
execFileorspawninstead ofexecwhen arguments come from user input — they avoid shell interpretation and injection attacks. - Always handle the
'error'event on spawned processes — it fires when the command is not found or cannot be executed. - Set
timeoutandmaxBufferonexec/execFileto prevent runaway processes and memory exhaustion. - Use
{ stdio: 'inherit' }for CLI tools that need to pass through terminal formatting and interactivity. - Forward
SIGTERM/SIGINTto child processes in your shutdown handler to avoid orphaned processes.
Common Pitfalls
- Shell injection — using
exec()with unsanitized user input allows arbitrary command execution. Always useexecFile()orspawn()with argument arrays. - Ignoring
maxBuffer—exec()defaults to 1 MB of buffered output; exceeding it kills the process silently. Increase the limit or usespawn()for large output. - Zombie processes — not handling child exit events or not killing children on parent exit leaves orphaned processes consuming resources.
- Assuming cross-platform commands —
ls,grep,catdo not exist on Windows. Use Node.js APIs or cross-platform tools when portability matters. - Not checking exit codes — a process can produce output on stdout and still fail. Always check the exit code in addition to reading output.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add nodejs-patterns-skills
Related Skills
Clustering
Cluster module patterns for scaling Node.js applications across multiple CPU cores
Error Handling
Comprehensive error handling strategies for robust and debuggable Node.js applications
Event Emitter
EventEmitter patterns for building decoupled, event-driven architectures in Node.js
File System
Modern fs/promises patterns for safe, efficient file system operations in Node.js
Native Modules
N-API and native addon patterns for extending Node.js with high-performance C/C++ and Rust modules
Streams
Node.js streams for efficient memory-conscious data processing with backpressure handling