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.
child_process.spawn(command[, args][, options])
When using
child_process.spawn(), the
options.stdioproperty 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.stdiovalue 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
stderrof the child process.
If
options.stdiois omitted, Node.js creates three
Streamobjects:
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
stdioarray 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.
nullor
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
stdiois 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
gdbon a compiled C child program (
child.c) shows the hang occurring at the
printfcall, confirming that the write blocks when the pipe buffer is full.
Unix Domain Socket Buffer
When
options.stdiois 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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.