The single-threaded nature of Node.js is great for I/O-bound tasks but can be a bottleneck for CPU-bound tasks (e.g., complex calculations, image processing, or data compression). If you run a long, synchronous calculation, you block the event loop, and your entire application freezes.

The solution is to delegate these heavy tasks to a separate process. Node's built-in child_process module allows you to create child processes to run other programs or scripts.

When to Use Child Processes

  • To perform a CPU-intensive operation without blocking the main event loop.
  • To run an external command-line utility (like git, ffmpeg, or python).
  • To take advantage of multiple CPU cores on a machine.

Key Functions in child_process

The module provides a few different ways to create child processes, each with its own use case.

  1. exec(): Spawns a shell and runs a command within that shell. It buffers the command's entire output and passes it to a callback function at the end. It's great for simple commands where you need the final output.

JavaScript


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

// Runs 'ls -lh' in a shell and buffers the output.
exec('ls -lh', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout:\n${stdout}`);
  if (stderr) {
    console.error(`stderr: ${stderr}`);
  }
});

Warning: Because exec uses a shell, it's vulnerable to shell injection if you pass unsanitized user input.

  1. spawn(): This is the most fundamental way to create a child process. It does not create a shell and streams the I/O data, making it ideal for long-running processes or commands that produce a large amount of data.

JavaScript


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

// The command and arguments are passed as an array, preventing shell injection.
const ls = spawn('ls', ['-lh', '/usr']);

// Listen to the stdout stream for data output.
ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

// Listen for the process to exit.
ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});
  1. fork(): A special version of spawn() specifically for creating new Node.js processes. The key benefit of fork() is that it establishes a built-in communication channel between the parent and child processes, allowing you to pass messages back and forth using .send() and .on('message'). This is the best way to create a "worker" process to offload JavaScript computations.
  • Parent Process (parent.js)

JavaScript


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

console.log('Parent process starting...');

const child = fork('./worker.js');

// Send a message to the child process.
child.send({ message: 'Start the heavy calculation!' });

// Listen for messages from the child.
child.on('message', (result) => {
  console.log(`Parent received result: ${result.sum}`);
  child.kill(); // Terminate the child when done.
});

console.log('Parent continues its own work...');
  • Child Process (worker.js)

JavaScript


// This function simulates a long, blocking operation.
const heavyCalculation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += 1;
  }
  return sum;
};

// Listen for messages from the parent.
process.on('message', (msg) => {
  console.log('Child received message:', msg.message);
  const result = heavyCalculation();
  // Send the result back to the parent.
  process.send({ sum: result });
});

By using fork(), the heavyCalculation() runs in a separate process, keeping the parent's event loop free and responsive.