NodeJSChild Processes

Child Processes

The node:child_process module lets a Node program spawn other programs — run a shell command, invoke an external binary like ffmpeg or git, or fork another Node script — each in its own fully isolated OS process. This is how you offload work that isn't JavaScript, isolate risky or crash-prone tasks, or use existing command-line tools. This page covers what a child process is, the four creation methods at a glance, communicating via stdio and messages, and the critical security rule about shell injection that catches developers out.

What a child process is

Text
  Parent (your Node app)                Child (separate OS process)
  ┌─────────────────────┐               ┌──────────────────────────┐
  │  spawn / exec / fork │ ───spawns──▶  │  ffmpeg / git / node ...  │
  │                      │               │  own memory, own PID      │
  │   stdin  ───────────▶│──── pipe ────▶│  stdin                    │
  │   stdout ◀───────────│◀─── pipe ─────│  stdout                   │
  │   stderr ◀───────────│◀─── pipe ─────│  stderr                   │
  └─────────────────────┘               └──────────────────────────┘
       Communicate over stdio streams (and IPC messages with fork)
A child process is a separate program with its own memory — isolated from the parent's event loop
A child process runs as an independent OS process with its own memory, its own PID, and (if it's another Node program) its own [event loop](/nodejs/event-loop) and V8 instance. Because it's separate, heavy work inside the child *does not block the parent's* event loop — the parent stays responsive while the child grinds. The parent and child communicate through the child's standard streams (`stdin`/`stdout`/`stderr`), which the parent can read and write as Node streams. This isolation is the whole point: crashes, memory leaks, and CPU spikes in the child are contained.
The four creation methods at a glance

Method

What it does

Use when

spawn

Launch a command, stream its output

Long-running / large output (stream it)

exec

Run a command in a shell, buffer all output

Short commands with small output

execFile

Run a binary directly (no shell)

Running an executable safely with args

fork

Spawn a Node script with an IPC channel

Parent↔child Node message passing

The next page details all four — `spawn` streams, `exec` buffers, `execFile` skips the shell, `fork` adds IPC
These four functions cover different needs and the [next page](/nodejs/spawn-exec-fork) explores each with examples. The essentials: `spawn` streams output and suits long-running processes or large output; `exec` runs through a shell and buffers everything into a callback (convenient but risky with untrusted input and bounded by a buffer limit); `execFile` runs a binary directly *without* a shell (safer); and `fork` is a specialization for spawning Node scripts that adds a built-in IPC message channel between parent and child.
Reading output from a child

JS
import { spawn } from 'node:child_process'

// Run 'git log' and stream its output line by line:
const child = spawn('git', ['log', '--oneline', '-n', '20'])

child.stdout.on('data', (chunk) => process.stdout.write(chunk))   // stream output
child.stderr.on('data', (chunk) => console.error('err:', chunk.toString()))

child.on('close', (code) => {
  console.log(`git exited with code ${code}`)
})
child.on('error', (err) => {
  console.error('failed to start subprocess:', err)   // e.g. command not found
})
a1b2c3d Fix pagination off-by-one
d4e5f6a Add Redis caching layer
...
git exited with code 0
Handle both the `error` event (failed to start) and the `close` event (exit code) — they're different
Two distinct failure signals must both be handled. The `error` event fires when the process *can't be started at all* — the binary doesn't exist, permission denied — and you won't get an exit code in that case. The `close` (or `exit`) event fires when a process that *did* start has finished, giving you the **exit code**: `0` means success, anything non-zero means the command itself failed (e.g. `git` returning 1). Always check the exit code rather than assuming success, and read `stderr` for the child's error output. Conflating "couldn't start" with "ran and failed" leads to confusing bugs.
The shell-injection danger

JS
import { exec, execFile } from 'node:child_process'

// ❌ DANGER: exec runs a SHELL — user input can inject commands:
const filename = userInput   // e.g. "photo.jpg; rm -rf /"
exec(`convert ${filename} out.png`)        // shell executes BOTH commands!

// ✅ SAFE: execFile passes args as a real array — no shell, no injection:
execFile('convert', [userInput, 'out.png'])  // userInput is ONE argument, inert

// ✅ SAFE: spawn with an args array (shell: false is the default):
spawn('convert', [userInput, 'out.png'])
Never pass untrusted input to `exec` — it runs a shell and is a command-injection hole; use `execFile`/`spawn` with an args array
`exec` (and `spawn`/`execFile` with `shell: true`) executes your command string through a **system shell**, which interprets metacharacters like `;`, `|`, `&&`, `$()`, and backticks. If any part of that string comes from user input, an attacker can append their own commands — `photo.jpg; rm -rf /` runs the delete. This is **command injection**, one of the most severe vulnerabilities ([security best practices](/nodejs/security-best-practices)). The fix: use `execFile` or `spawn` with the command and an **array of arguments** and *no shell* (the default) — each array element is passed as a single, literal argument the shell never parses. Avoid string interpolation of untrusted data into commands entirely.
When to use a child process
  • Invoking external toolsffmpeg, imagemagick, git, pandoc; reuse a mature binary instead of reimplementing it.

  • Isolating crash-prone or risky work — a segfault or runaway in the child can't take down the parent.

  • CPU-bound work you want fully isolated — though worker threads are lighter for in-process compute.

  • Running a different language/runtime — call a Python script, a Go binary, a shell pipeline.

  • Sandboxing — run untrusted code with restricted permissions in its own process.

Spawning is not free — process startup and IPC have real overhead; don't spawn per request
Creating an OS process is far heavier than calling a function: it has startup latency (especially `fork`ing a Node script, which boots a new V8) and memory cost, and inter-process communication is slower than in-memory calls. Spawning a fresh child *per request* will crush throughput and can exhaust system resources. For repeated work, maintain a **pool** of long-lived child processes (or use [worker threads](/nodejs/worker-threads) for in-process CPU tasks), reuse them, and cap how many run concurrently. Also always clean up: kill orphaned children on shutdown so they don't linger.
Next
The four methods in depth: [spawn, exec, execFile & fork](/nodejs/spawn-exec-fork).