Backend Development 13 min read

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.

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

login 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.