Back to Blog
Node.jsChild ProcessesParallelSystem

Node.js Child Processes

Master Node.js child processes. From exec to spawn to fork for running external commands and parallel processing.

B
Bootspring Team
Engineering
July 24, 2020
7 min read

Child processes allow Node.js to run external commands and parallelize work. Here's how to use them effectively.

Overview of Methods#

1const { exec, execFile, spawn, fork } = require('child_process'); 2 3// exec: Run command in shell, buffer output 4// execFile: Run executable directly, buffer output 5// spawn: Run command, stream output 6// fork: Special spawn for Node.js scripts with IPC

Using exec#

1const { exec } = require('child_process'); 2 3// Basic usage 4exec('ls -la', (error, stdout, stderr) => { 5 if (error) { 6 console.error(`Error: ${error.message}`); 7 return; 8 } 9 if (stderr) { 10 console.error(`stderr: ${stderr}`); 11 return; 12 } 13 console.log(`stdout: ${stdout}`); 14}); 15 16// With options 17exec('cat package.json', { 18 cwd: '/path/to/project', 19 encoding: 'utf8', 20 maxBuffer: 1024 * 1024, // 1MB 21 timeout: 5000, 22 env: { ...process.env, NODE_ENV: 'production' }, 23}, (error, stdout, stderr) => { 24 console.log(stdout); 25}); 26 27// Promise wrapper 28const { promisify } = require('util'); 29const execAsync = promisify(exec); 30 31async function runCommand(cmd) { 32 try { 33 const { stdout, stderr } = await execAsync(cmd); 34 return stdout.trim(); 35 } catch (error) { 36 throw new Error(`Command failed: ${error.message}`); 37 } 38} 39 40// Usage 41const gitBranch = await runCommand('git branch --show-current'); 42console.log(`Current branch: ${gitBranch}`);

Using execFile#

1const { execFile } = require('child_process'); 2 3// More secure - doesn't use shell 4execFile('node', ['--version'], (error, stdout, stderr) => { 5 if (error) { 6 console.error(error); 7 return; 8 } 9 console.log(`Node version: ${stdout}`); 10}); 11 12// Run a script 13execFile('./script.sh', ['arg1', 'arg2'], { 14 cwd: __dirname, 15}, (error, stdout, stderr) => { 16 console.log(stdout); 17}); 18 19// Promise version 20const { promisify } = require('util'); 21const execFileAsync = promisify(execFile); 22 23async function runScript() { 24 const { stdout } = await execFileAsync('python3', ['script.py', '--flag']); 25 return JSON.parse(stdout); 26}

Using spawn#

1const { spawn } = require('child_process'); 2 3// Basic spawn 4const ls = spawn('ls', ['-la']); 5 6ls.stdout.on('data', (data) => { 7 console.log(`stdout: ${data}`); 8}); 9 10ls.stderr.on('data', (data) => { 11 console.error(`stderr: ${data}`); 12}); 13 14ls.on('close', (code) => { 15 console.log(`Process exited with code ${code}`); 16}); 17 18// With options 19const child = spawn('npm', ['install'], { 20 cwd: '/path/to/project', 21 env: { ...process.env, CI: 'true' }, 22 stdio: 'inherit', // Inherit parent's stdio 23}); 24 25// Pipe streams 26const grep = spawn('grep', ['pattern']); 27const cat = spawn('cat', ['file.txt']); 28 29cat.stdout.pipe(grep.stdin); 30 31grep.stdout.on('data', (data) => { 32 console.log(`Found: ${data}`); 33}); 34 35// Detached process 36const child = spawn('long-running-process', [], { 37 detached: true, 38 stdio: 'ignore', 39}); 40 41child.unref(); // Allow parent to exit 42 43// Shell command 44const child = spawn('echo "Hello" && echo "World"', { 45 shell: true, 46});

Using fork#

1// parent.js 2const { fork } = require('child_process'); 3 4const child = fork('worker.js'); 5 6// Send message to child 7child.send({ type: 'START', data: [1, 2, 3, 4, 5] }); 8 9// Receive messages from child 10child.on('message', (message) => { 11 console.log('Result from child:', message); 12}); 13 14child.on('exit', (code) => { 15 console.log(`Child exited with code ${code}`); 16}); 17 18// worker.js 19process.on('message', (message) => { 20 if (message.type === 'START') { 21 const result = message.data.reduce((a, b) => a + b, 0); 22 process.send({ type: 'RESULT', data: result }); 23 } 24}); 25 26// With module path 27const worker = fork('./worker.js', ['--arg1'], { 28 cwd: __dirname, 29 env: { ...process.env, WORKER_ID: '1' }, 30 execArgv: ['--max-old-space-size=4096'], 31});

Process Pool#

1const { fork } = require('child_process'); 2const os = require('os'); 3 4class ProcessPool { 5 constructor(workerPath, poolSize = os.cpus().length) { 6 this.workerPath = workerPath; 7 this.poolSize = poolSize; 8 this.workers = []; 9 this.taskQueue = []; 10 this.activeWorkers = new Map(); 11 12 this.initialize(); 13 } 14 15 initialize() { 16 for (let i = 0; i < this.poolSize; i++) { 17 this.createWorker(); 18 } 19 } 20 21 createWorker() { 22 const worker = fork(this.workerPath); 23 24 worker.on('message', (result) => { 25 const { resolve, reject } = this.activeWorkers.get(worker); 26 27 if (result.error) { 28 reject(new Error(result.error)); 29 } else { 30 resolve(result.data); 31 } 32 33 this.activeWorkers.delete(worker); 34 this.workers.push(worker); 35 this.processQueue(); 36 }); 37 38 worker.on('error', (error) => { 39 const task = this.activeWorkers.get(worker); 40 if (task) { 41 task.reject(error); 42 this.activeWorkers.delete(worker); 43 } 44 this.createWorker(); // Replace dead worker 45 }); 46 47 this.workers.push(worker); 48 } 49 50 async execute(data) { 51 return new Promise((resolve, reject) => { 52 this.taskQueue.push({ data, resolve, reject }); 53 this.processQueue(); 54 }); 55 } 56 57 processQueue() { 58 while (this.workers.length > 0 && this.taskQueue.length > 0) { 59 const worker = this.workers.pop(); 60 const task = this.taskQueue.shift(); 61 62 this.activeWorkers.set(worker, task); 63 worker.send(task.data); 64 } 65 } 66 67 shutdown() { 68 for (const worker of this.workers) { 69 worker.kill(); 70 } 71 for (const worker of this.activeWorkers.keys()) { 72 worker.kill(); 73 } 74 } 75} 76 77// worker.js 78process.on('message', async (data) => { 79 try { 80 const result = await processData(data); 81 process.send({ data: result }); 82 } catch (error) { 83 process.send({ error: error.message }); 84 } 85}); 86 87// Usage 88const pool = new ProcessPool('./worker.js', 4); 89 90const results = await Promise.all([ 91 pool.execute({ task: 'compute', value: 100 }), 92 pool.execute({ task: 'compute', value: 200 }), 93 pool.execute({ task: 'compute', value: 300 }), 94]);

Streaming Large Data#

1const { spawn } = require('child_process'); 2const fs = require('fs'); 3 4// Process large file with external tool 5function processLargeFile(inputPath, outputPath) { 6 return new Promise((resolve, reject) => { 7 const input = fs.createReadStream(inputPath); 8 const output = fs.createWriteStream(outputPath); 9 10 const gzip = spawn('gzip', ['-c']); 11 12 input.pipe(gzip.stdin); 13 gzip.stdout.pipe(output); 14 15 gzip.on('close', (code) => { 16 if (code === 0) { 17 resolve(); 18 } else { 19 reject(new Error(`gzip exited with code ${code}`)); 20 } 21 }); 22 23 gzip.on('error', reject); 24 }); 25} 26 27// FFmpeg example 28function convertVideo(input, output) { 29 return new Promise((resolve, reject) => { 30 const ffmpeg = spawn('ffmpeg', [ 31 '-i', input, 32 '-c:v', 'libx264', 33 '-preset', 'fast', 34 '-c:a', 'aac', 35 output, 36 ]); 37 38 ffmpeg.stderr.on('data', (data) => { 39 // FFmpeg outputs progress to stderr 40 const progress = parseProgress(data.toString()); 41 if (progress) { 42 console.log(`Progress: ${progress}%`); 43 } 44 }); 45 46 ffmpeg.on('close', (code) => { 47 code === 0 ? resolve() : reject(new Error('Conversion failed')); 48 }); 49 }); 50}

Error Handling#

1const { spawn, exec } = require('child_process'); 2 3// Comprehensive error handling 4function runCommand(command, args = []) { 5 return new Promise((resolve, reject) => { 6 const child = spawn(command, args); 7 8 let stdout = ''; 9 let stderr = ''; 10 11 child.stdout.on('data', (data) => { 12 stdout += data; 13 }); 14 15 child.stderr.on('data', (data) => { 16 stderr += data; 17 }); 18 19 child.on('error', (error) => { 20 reject(new Error(`Failed to start: ${error.message}`)); 21 }); 22 23 child.on('close', (code, signal) => { 24 if (signal) { 25 reject(new Error(`Process killed with signal ${signal}`)); 26 } else if (code !== 0) { 27 const error = new Error(`Process exited with code ${code}`); 28 error.stdout = stdout; 29 error.stderr = stderr; 30 reject(error); 31 } else { 32 resolve({ stdout, stderr }); 33 } 34 }); 35 }); 36} 37 38// Timeout handling 39function runWithTimeout(command, timeout = 30000) { 40 return new Promise((resolve, reject) => { 41 const child = exec(command, { timeout }); 42 43 child.on('error', reject); 44 45 child.on('close', (code, signal) => { 46 if (signal === 'SIGTERM') { 47 reject(new Error('Process timed out')); 48 } else if (code === 0) { 49 resolve(); 50 } else { 51 reject(new Error(`Exit code: ${code}`)); 52 } 53 }); 54 }); 55}

IPC Communication#

1// parent.js 2const { fork } = require('child_process'); 3 4const child = fork('worker.js', [], { 5 serialization: 'advanced', // Support more data types 6}); 7 8// Send various data types 9child.send({ 10 buffer: Buffer.from('hello'), 11 date: new Date(), 12 map: new Map([['key', 'value']]), 13}); 14 15// Bidirectional communication 16child.on('message', (msg) => { 17 if (msg.type === 'REQUEST_DATA') { 18 child.send({ type: 'DATA', payload: getData() }); 19 } 20}); 21 22// Send file descriptor 23const net = require('net'); 24const server = net.createServer(); 25 26server.listen(0, () => { 27 child.send('server', server); // Pass server handle 28}); 29 30// worker.js 31process.on('message', (msg, handle) => { 32 if (handle) { 33 // Received a socket or server 34 handle.on('connection', (socket) => { 35 // Handle connection in child process 36 }); 37 } 38});

Cluster Module Integration#

1const cluster = require('cluster'); 2const http = require('http'); 3const os = require('os'); 4 5if (cluster.isPrimary) { 6 const numCPUs = os.cpus().length; 7 8 console.log(`Primary ${process.pid} is running`); 9 10 // Fork workers 11 for (let i = 0; i < numCPUs; i++) { 12 cluster.fork(); 13 } 14 15 cluster.on('exit', (worker, code, signal) => { 16 console.log(`Worker ${worker.process.pid} died`); 17 cluster.fork(); // Replace dead worker 18 }); 19 20 // Message from workers 21 cluster.on('message', (worker, message) => { 22 console.log(`Message from worker ${worker.id}:`, message); 23 }); 24 25} else { 26 // Workers share TCP connection 27 http.createServer((req, res) => { 28 res.writeHead(200); 29 res.end(`Hello from worker ${cluster.worker.id}\n`); 30 }).listen(8000); 31 32 console.log(`Worker ${process.pid} started`); 33 34 // Send message to primary 35 process.send({ type: 'ready', pid: process.pid }); 36}

Best Practices#

Method Selection: ✓ exec: Simple commands, need shell features ✓ execFile: Known executables, security matters ✓ spawn: Large output, streaming needed ✓ fork: Node.js scripts with IPC Security: ✓ Prefer execFile over exec ✓ Validate user input ✓ Use shell: false when possible ✓ Set appropriate cwd and env Performance: ✓ Use process pools for heavy work ✓ Stream large data instead of buffering ✓ Limit concurrent processes ✓ Handle cleanup properly Error Handling: ✓ Listen to 'error' events ✓ Check exit codes and signals ✓ Handle timeouts ✓ Log stderr appropriately

Conclusion#

Child processes enable Node.js to run external commands and parallelize CPU-intensive work. Use exec for simple shell commands, spawn for streaming, and fork for Node.js worker processes with IPC. Implement process pools for heavy workloads and always handle errors appropriately.

Share this article

Help spread the word about Bootspring