spawn, exec, execFile & fork
The child_process module offers four ways to start a child, and choosing the right one matters for performance, safety, and ergonomics. The core axes are: does it stream output or buffer it all into memory? does it run through a shell (convenient but injectable) or execute a binary directly (safe)? and does it set up an IPC channel for message passing? This page compares all four, shows each in action, and gives a clear decision rule.
The comparison
Output | Shell? | IPC? | Best for | |
|---|---|---|---|---|
| Streamed | No (default) | No | Long-running, large/continuous output |
| Buffered | Yes | No | Short shell commands, small output |
| Buffered | No | No | Running a binary safely with args |
| Streamed | No | Yes | Spawning Node scripts that message back |
spawn — stream a long-running command
import { spawn } from 'node:child_process'
// Transcode a video — output streams, so memory stays flat even for huge files:
const ff = spawn('ffmpeg', ['-i', 'input.mov', '-c:v', 'libx264', 'output.mp4'])
ff.stderr.on('data', (d) => process.stdout.write(d)) // ffmpeg logs progress to stderr
ff.on('close', (code) => console.log(code === 0 ? 'done' : `failed (${code})`))exec — buffer the output of a short command
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
const execAsync = promisify(exec)
// Convenient for short commands whose output fits comfortably in memory:
const { stdout } = await execAsync('git rev-parse --short HEAD')
console.log('commit:', stdout.trim())
// You CAN cap the buffer to avoid surprises:
exec('some-command', { maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => { /* ... */ })execFile — run a binary safely
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
const execFileAsync = promisify(execFile)
// No shell: 'userFile' is passed as a single literal argument — injection-proof:
const { stdout } = await execFileAsync('identify', ['-format', '%wx%h', userFile])
console.log('dimensions:', stdout) // e.g. "1920x1080"fork — spawn a Node script with IPC
parent.js
import { fork } from 'node:child_process'
const child = fork('./worker.js')
// Send a structured message (objects are serialized automatically):
child.send({ cmd: 'sum', numbers: [1, 2, 3, 4, 5] })
// Receive the child's reply over the IPC channel:
child.on('message', (msg) => {
console.log('result from child:', msg.result) // 15
child.disconnect()
})worker.js
process.on('message', (msg) => {
if (msg.cmd === 'sum') {
const result = msg.numbers.reduce((a, b) => a + b, 0)
process.send({ result }) // send the answer back to the parent
}
})The decision rule
Large or continuous output? →
spawn(stream it).Short command, small output, trusted string? →
exec(convenient).Running a binary with user-supplied args? →
execFile(no shell, no injection).Spawning a Node script that must message back? →
fork(built-in IPC).Just need parallel CPU compute in-process? → not child_process at all — use worker threads.