devnotes.

Node Child Processes

Node.js runs your JavaScript on a single thread. That's fine for I/O-heavy work, but what about a genuinely CPU-intensive task, or running some other program entirely — a Python script, a shell command, an image-conversion tool? That's exactly what the child_process module is for: spinning up a separate process alongside your main one.

Four ways to create a child process, and when each fits

1. exec() — runs a shell command and buffers up its entire output at once. Best for short commands with small output.

const { exec } = require('child_process');

exec('node -v', (error, stdout, stderr) => {
  if (error) return console.error(error);
  console.log('Node version:', stdout.trim());
});

2. execFile() — runs an executable file directly, without going through a shell. Slightly faster and safer than exec() since there's no shell string parsing involved.

const { execFile } = require('child_process');

execFile('git', ['--version'], (error, stdout) => {
  if (error) return console.error(error);
  console.log(stdout);
});

3. spawn() — streams output as it happens instead of buffering it all, making it the right choice for long-running processes or large amounts of output.

const { spawn } = require('child_process');

const ping = spawn('ping', ['-c', '4', 'example.com']);

ping.stdout.on('data', (chunk) => {
  console.log(chunk.toString());
});

ping.on('close', (code) => {
  console.log(`ping exited with code ${code}`);
});

4. fork() — a special case built specifically for launching another Node.js file, with a built-in messaging channel between parent and child.

// parent.js
const { fork } = require('child_process');
const child = fork('worker.js');

child.on('message', (result) => {
  console.log('Result from child:', result);
});

child.send({ number: 39 });
// worker.js — runs as a fully separate process
process.on('message', (msg) => {
  const isPrime = checkPrime(msg.number); // some expensive calculation
  process.send(isPrime);
});

function checkPrime(n) {
  for (let i = 2; i < n; i++) if (n % i === 0) return false;
  return n > 1;
}

Because worker.js runs in its own process with its own thread, the expensive checkPrime calculation never blocks the main event loop in parent.js — the two processes communicate purely through .send() and the 'message' event.

Choosing between them at a glance

MethodShell involvedStreamingBest for
exec()YesNo (buffered)Short shell commands
execFile()NoNo (buffered)Running a specific executable directly
spawn()No, by defaultYesLong-running processes, large output
fork()NoVia messagesRunning another Node.js script with IPC

Where this actually gets used

Offloading a heavy computation (image resizing, video processing, complex data crunching) into a forked or spawned process keeps your main server responsive to every other request while that work happens elsewhere — the same core philosophy behind Node.js's whole non-blocking design, just applied at the process level instead of the callback level.

Node Child Processes | DevNotes