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
| Method | Shell involved | Streaming | Best for |
|---|---|---|---|
exec() | Yes | No (buffered) | Short shell commands |
execFile() | No | No (buffered) | Running a specific executable directly |
spawn() | No, by default | Yes | Long-running processes, large output |
fork() | No | Via messages | Running 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.