Skip to content
Node.js nd process 5 min read

Spawning Processes with spawn & fork

Sometimes a Node.js program needs to launch another program — a shell utility, a media encoder, a long-running build tool, or even another Node script — and react to its output as it arrives rather than waiting for it to finish. The node:child_process module provides several ways to do this, but spawn and fork are the two you reach for when you care about streaming and inter-process communication. This page explains how they work, how to pass arguments safely, and when to choose them over exec.

How spawn works

spawn starts a new process and gives you back a ChildProcess object whose stdout, stderr, and stdin are streams. Because data is streamed rather than buffered into a single string, spawn is ideal for long-running commands or commands that produce large amounts of output — you process each chunk as it appears instead of holding the whole result in memory.

spawn does not run the command inside a shell by default. You pass the executable name and an array of arguments separately, which means there is no shell interpolation and therefore no shell-injection risk from unescaped user input.

import { spawn } from "node:child_process";

const child = spawn("ping", ["-c", "3", "example.com"]);

child.stdout.on("data", (chunk) => {
  process.stdout.write(`out: ${chunk}`);
});

child.stderr.on("data", (chunk) => {
  process.stderr.write(`err: ${chunk}`);
});

child.on("close", (code) => {
  console.log(`process exited with code ${code}`);
});

Output:

out: PING example.com (93.184.216.34): 56 data bytes
out: 64 bytes from 93.184.216.34: icmp_seq=0 ttl=56 time=11.4 ms
out: 64 bytes from 93.184.216.34: icmp_seq=1 ttl=56 time=10.9 ms
out: 64 bytes from 93.184.216.34: icmp_seq=2 ttl=56 time=11.1 ms
process exited with code 0

The data events deliver Buffer objects. Call child.stdout.setEncoding("utf8") if you would rather receive strings, or collect the chunks and decode them yourself.

Pass { shell: true } only when you genuinely need shell features like pipes or globbing. Doing so reopens the door to command injection — never interpolate untrusted input into a shell command string.

Passing arguments and options

Arguments always go in the second parameter as an array. The third parameter is an options object that controls the child’s environment, working directory, and how its stdio is wired up.

import { spawn } from "node:child_process";

const child = spawn("git", ["log", "--oneline", "-n", "5"], {
  cwd: "/path/to/repo",
  env: { ...process.env, GIT_PAGER: "cat" },
  stdio: ["ignore", "pipe", "inherit"],
});

let log = "";
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => (log += chunk));
child.on("close", () => console.log(log.trim()));

The stdio array maps the child’s stdin, stdout, and stderr. Common values:

ValueMeaning
"pipe"Create a stream you read/write from the parent (default)
"inherit"Share the parent’s own stdin/stdout/stderr
"ignore"Discard the stream (/dev/null)
"ipc"Open an IPC channel (used internally by fork)

Setting stdio: "inherit" is a shortcut that wires all three streams straight to the parent, so the child’s output appears in your terminal with no manual piping.

Forking a Node process with IPC

fork is a specialization of spawn for launching a new Node.js process that runs a given module. On top of the normal stdio, it automatically opens an IPC channel, so the parent and child can exchange structured messages with send() and the "message" event. Values are serialized for you, so you can pass plain objects.

// parent.mjs
import { fork } from "node:child_process";

const child = fork("./worker.mjs");

child.on("message", (msg) => {
  console.log("parent received:", msg);
  child.disconnect();
});

child.send({ task: "sum", numbers: [1, 2, 3, 4] });
// worker.mjs
process.on("message", (msg) => {
  if (msg.task === "sum") {
    const total = msg.numbers.reduce((a, b) => a + b, 0);
    process.send({ result: total });
  }
});

Output:

parent received: { result: 10 }

Each forked process is a fresh V8 instance with its own memory and event loop, which makes fork a simple way to offload CPU-bound work without blocking the main process. For tighter, lower-overhead parallelism within one process, consider node:worker_threads instead — forks are heavier because they are full OS processes.

Always tear down forked children. Call child.disconnect() or child.kill() when work is done, and listen for "exit"; orphaned child processes keep the parent’s event loop alive and leak resources.

Choosing spawn vs exec vs fork

All three live in node:child_process but solve different problems.

MethodBest forOutput handlingShellIPC
spawnLong-running commands, large or streaming outputStreamed via stdio streamsNo (unless shell: true)No
execShort commands whose full output fits in memoryBuffered into a single callback (stdout, stderr)Yes (runs in a shell)No
forkRunning another Node module as a subprocessStreamed, plus an IPC channelNoYes

In short: use spawn when you want to stream, exec when you just want a command’s complete output as a string and the convenience of shell syntax, and fork when the child is itself Node and you want to message it. execFile sits between spawn and exec — it buffers like exec but skips the shell like spawn.

Best practices

  • Prefer the argument-array form of spawn/fork over shell: true to eliminate command-injection risk.
  • Stream and process output incrementally for long-running or high-volume commands instead of buffering it all.
  • Always handle the "error" event — it fires when the executable cannot be found or fails to spawn, separate from a non-zero exit code.
  • Inspect the code and signal arguments of the "close"/"exit" events to distinguish clean exits from crashes or kills.
  • Clean up children explicitly with kill() or disconnect(), and propagate shutdown signals so subprocesses do not become orphans.
  • Reach for worker_threads instead of fork when you need shared-memory or many lightweight workers; reserve fork for true process isolation.
Last updated June 14, 2026
Was this helpful?