Why Node.js Child Processes Hang: Understanding stdio, Pipes, and Data Consumption

This article explains how Node.js child_process.spawn handles stdio options, why failing to attach 'data' listeners or consume pipe streams can cause the child to block, and demonstrates experiments, debugging techniques, and proper configuration such as using 'ignore' or 'pipe' to avoid deadlocks.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
Why Node.js Child Processes Hang: Understanding stdio, Pipes, and Data Consumption

child_process.spawn(command[, args][, options])

When using child_process.spawn(), the options.stdio property determines how the child process's standard streams are connected. It can be a string or an array. command: the command to execute. args: optional command‑line arguments. options: additional options, most importantly stdio.

The options.stdio value may be a string such as 'pipe', 'inherit', 'ignore', or an array like ['pipe','pipe','pipe']. When it is an array, each element corresponds to a file descriptor (fd) for stdin, stdout, and stderr of the child process.

If options.stdio is omitted, Node.js creates three Stream objects: child.stdin, child.stdout, and child.stderr. The default configuration is equivalent to ['pipe','pipe','pipe'], meaning each stream is a pipe that connects the parent and child processes.

Each element of the stdio array can be one of the following: 'pipe': creates a pipe between the parent and child; the child’s fd is exposed as child.stdio[0‑2] and maps to child.stdin, child.stdout, child.stderr. 'ipc': creates an IPC channel (used only for Node.js processes, not for stdio). 'ignore': redirects the fd to /dev/null. 'inherit': inherits the corresponding fd from the parent process. Stream: a readable or writable stream object (TTY, file, socket, pipe, etc.) that has an underlying fd.

Positive integer: a raw file descriptor. null or undefined: leaves the fd at its default value ( 'pipe' for the first three, 'ignore' for the rest).

If a child process writes to a pipe whose output is not being consumed, the pipe’s buffer (typically 64 KB on Linux) will eventually fill, causing the child to block. This is why attaching a on('data') listener—or otherwise consuming the stream—is essential when stdio is set to 'pipe' or 'inherit'.

Experiment

We create a simple child script ( child.js) that writes to stdout. In the parent script ( index.js) we first run the child with the default configuration and observe the expected output.

Adding a child.stdout.on('data', ...) handler consumes the data and the process exits cleanly. Removing the handler while the child writes a large amount of data causes the pipe buffer to fill and the child process hangs.

Debugging with gdb on a compiled C child program ( child.c) shows the hang occurring at the printf call, confirming that the write blocks when the pipe buffer is full.

Unix Domain Socket Buffer

When options.stdio is set to 'pipe', Node.js actually creates a Unix Domain Socket with a default buffer size of 65 536 bytes. If the child writes more than this without the parent reading, the buffer fills and the child blocks.

Why the Parent Must Read

Node.js streams start in a paused state. Adding a 'data' listener (or calling stream.resume() or stream.pipe()) switches the stream to flowing mode, allowing data to be read. Without a listener, the internal buffer grows until it reaches the high‑water mark (default 16 KB). Once the buffer is full, the child’s write call blocks.

All Readable streams begin in paused mode but can be switched to flowing mode in one of the following ways: Adding a 'data' event handler. Calling the stream.resume() method. Calling the stream.pipe() method to send the data to a Writable.

The Node.js documentation states:

By default, pipes for stdin, stdout, and stderr are established between the parent Node.js process and the spawned child. These pipes have limited (and platform‑specific) capacity. If the child process writes to stdout in excess of that limit without the output being captured, the child process will block waiting for the pipe buffer to accept more data. This is identical to the behavior of pipes in the shell. Use the { stdio: 'ignore' } option if the output will not be consumed.

Therefore, when spawning a child with stdio: 'pipe', you must either consume the data (e.g., attach on('data')) or explicitly ignore the streams using { stdio: 'ignore' } to prevent the child from hanging.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

debuggingNode.jsStreamUnix Domain Socketchild_processPipeSTDIO
Taobao Frontend Technology
Written by

Taobao Frontend Technology

The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.