Node.js’s child_process module lets you spawn new processes, but the real magic is how those processes can talk to each other.

Let’s see it in action. Imagine a main process that needs to do some heavy lifting, but doesn’t want to block the main thread. It can offload that work to a child process.

Here’s a simple parent script (parent.js):

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

const child = spawn('node', ['child.js']);

child.stdout.on('data', (data) => {
  console.log(`[Parent] Received from child: ${data}`);
});

child.on('close', (code) => {
  console.log(`[Parent] Child process exited with code ${code}`);
});

child.stdin.write('Hello from parent!\n');

And here’s the child script (child.js):

process.stdin.on('data', (data) => {
  const message = data.toString().trim();
  console.log(`[Child] Received from parent: ${message}`);
  process.stdout.write(`Acknowledged: ${message}\n`);
});

When you run node parent.js, you’ll see output like this:

[Parent] Received from child: Acknowledged: Hello from parent!
[Parent] Child process exited with code 0

The parent process spawns child.js. It then sets up a listener on child.stdout to capture anything the child writes to its standard output. It also listens for the close event, which fires when the child process finishes. Crucially, the parent writes directly to the child’s standard input using child.stdin.write().

The child process, in turn, listens for data on process.stdin. When it receives data, it logs it and then writes an acknowledgment back to its own standard output (process.stdout.write()), which the parent is listening to.

This is the most basic form of Inter-Process Communication (IPC) in Node.js: using standard input/output streams. The child_process module provides several ways to manage these streams, including pipe, ignore, and inherit, allowing fine-grained control over how parent and child processes interact.

Beyond raw streams, Node.js offers a more robust IPC mechanism specifically for forked processes (a specialized version of spawn for Node.js scripts). When you fork a process, Node.js automatically sets up an IPC channel.

Consider this parent_fork.js:

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

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

child.on('message', (msg) => {
  console.log(`[Parent] Message from child: ${JSON.stringify(msg)}`);
});

child.send({ greeting: 'Hello from parent via send!' });

child.on('exit', (code) => {
  console.log(`[Parent] Child process exited with code ${code}`);
});

And child_fork.js:

process.on('message', (msg) => {
  console.log(`[Child] Message from parent: ${JSON.stringify(msg)}`);
  process.send({ response: 'Message received by child!' });
});

Running node parent_fork.js yields:

[Child] Message from parent: {"greeting":"Hello from parent via send!"}
[Parent] Message from child: {"response":"Message received by child!"}
[Parent] Child process exited with code 0

Here, fork is used. The key difference is the child.on('message', ...) and child.send(...) methods. These abstract away the stream management and provide a convenient, event-driven way to send structured data (JavaScript objects) between processes. The process.on('message', ...) and process.send(...) in the child mirror this functionality. This send/message mechanism uses a hidden IPC channel managed by Node.js, which is generally more efficient and easier to use for Node-to-Node communication than raw streams.

The send method can serialize and deserialize JavaScript objects, making it powerful for passing complex data structures. Internally, this IPC channel is implemented using Unix domain sockets on POSIX systems and named pipes on Windows.

The primary problem this solves is avoiding blocking the main Node.js event loop when performing I/O-bound or CPU-intensive tasks. By delegating these tasks to child processes, the main process remains responsive. Furthermore, it allows for leveraging multiple CPU cores, as each child process can run on a separate core.

A common misunderstanding is that spawn and fork are interchangeable. While spawn is more general and can execute any command, fork is specifically designed for spawning new Node.js processes. fork also implicitly sets up the IPC channel, making the send/message communication available out-of-the-box, which isn’t the case with spawn unless you manually set up the channel.

When you use child.send(message) in the parent and process.send(message) in the child, Node.js handles the serialization and deserialization of the message object. It uses the MessageChannel API internally, which is a more sophisticated mechanism than simple stream piping. This channel is established when the child process is forked and remains open for the lifetime of the child process, allowing for bidirectional communication.

The next hurdle is understanding how to manage multiple worker processes for better load balancing and fault tolerance.

Want structured learning?

Take the full Nodejs course →