Child processes — spawn, exec, and fork
Node's single-threaded model means genuinely running another program, or truly parallel Node code, requires an entirely separate OS process — child_process is the API for creating and communicating with one, and the three main functions each fit a different real shape of that need.
3 min read
spawn: streaming output from a long-running or high-output command
const { spawn } = require("node:child_process");
const child = spawn("ping", ["example.com"]);
child.stdout.on("data", (chunk) => console.log(`output: ${chunk}`)); // stdout is a STREAM — chunks arrive over time
child.stderr.on("data", (chunk) => console.error(`error: ${chunk}`));
child.on("close", (code) => console.log(`process exited with code ${code}`));spawn launches a new process and gives back streaming access to its stdout/stderr — the right choice for a long-running command, or one that produces a large amount of output, since the data arrives incrementally (the same streaming model this domain's earlier streams lesson covered) rather than being buffered entirely in memory before anything's available. ping, a build tool, a long-running data-processing script — anything where output should be handled as it's produced, not all at once at the end.
exec: convenient for a short command whose full output is needed at once
const { exec } = require("node:child_process");
exec("ls -la", (err, stdout, stderr) => {
if (err) { console.error(err); return; }
console.log(stdout); // the FULL output, buffered and handed back all at once
});exec runs a command through a shell (meaning shell features like pipes and wildcards work directly in the command string) and buffers the entire output before calling back with it — simpler to use for a short command where the full result is needed anyway, but a real, practical limit exists: exec's default output buffer has a maximum size, and a command producing more output than that limit causes an error, which spawn's streaming approach never runs into since it never needs the whole output in memory at once.
fork: specifically for spawning another Node.js process, with a real message channel
// parent.js
const { fork } = require("node:child_process");
const child = fork("./worker.js");
child.send({ task: "process-data", payload: [1, 2, 3] }); // structured message, not just a raw stdout string
child.on("message", (result) => console.log("got result:", result));
// worker.js
process.on("message", (msg) => {
const result = msg.payload.map((n) => n * 2);
process.send(result); // sends a structured message BACK to the parent
});fork is a specialized version of spawn specifically for launching another Node.js script, and it automatically sets up a built-in communication channel (.send()/process.on("message", ...)) for passing structured JavaScript objects back and forth — not raw text output that has to be parsed, the way spawn's stdout would require. This is the real, deliberate tool for splitting CPU-bound work (recall the earlier non-blocking-I/O lesson's point that non-blocking I/O does nothing for genuinely CPU-heavy work) off into a fully separate process, with structured communication built in rather than reinvented on top of raw stdout parsing.
Why this matters: genuine parallelism, unlike anything covered earlier in this domain
Every mechanism covered in this domain until now — the event loop, non-blocking I/O, streams — describes clever scheduling on a single thread, never genuine simultaneous execution. A child process is a real, separate OS process, with its own memory space and its own thread, actually running at the same time as the parent — true parallelism, at the cost of real overhead (process creation, and structured communication between processes requires explicit message-passing, since they don't share memory the way threads within one process would). The next lesson, worker threads, covers a lighter-weight alternative for CPU-bound parallelism specifically within a single Node application.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is `spawn` the right choice for a long-running or high-output command, rather than `exec`?
2. What makes `fork` different from a generic `spawn` call?
3. What does a child process provide that everything covered earlier in this domain (event loop, streams, non-blocking I/O) cannot?