Why Your Node.js Child Process Hangs: Mastering stdio, Pipes, and Stream Consumption
This article explains how Node.js child_process stdio options, pipe handling, and stream events interact, why unconsumed output can block a child process, and provides step‑by‑step experiments and debugging techniques to avoid deadlocks.
child_process.spawn(command[, args][, options])
We focus on the options.stdio property of child_process.spawn(). It can be a string or an array, with values such as inherit, pipe, ignore, etc.
Understanding options.stdio
If options.stdio is an array, each element defines the file descriptor (fd) for the child process. By default, spawn() creates three streams: child.stdin, child.stdout, and child.stderr. These streams are linked to the parent process according to the stdio configuration.
When options.stdio is a string, it applies the same meaning to all three fds. For example, 'pipe' is equivalent to ['pipe', 'pipe', 'pipe']. 'pipe': creates a pipe between parent and child; the child’s stdio streams are exposed as child.stdio[0-2] and are redirected to the parent’s corresponding streams. 'ipc': creates an IPC channel (only for Node.js processes) and is not used for stdio streams. 'ignore': redirects the fd to /dev/null. 'inherit': inherits the parent’s corresponding fd; for the first three fds it behaves like inherit, later fds act like ignore. Stream: a readable or writable stream object (TTY, file, socket, pipe) that must have an underlying fd.
Positive integers: represent a specific file descriptor. null / undefined: keep the default value (first three fds default to pipe, later ones to ignore).
Experiment: Consuming Pipe Output
We create a child script child.js and a parent script index.js. Running the parent with a simple console.log('hello') works fine. Adding a child.stdout.on('data', ...) handler and outputting more data shows that the child process blocks when the pipe buffer (default 65536 bytes on Linux) fills up and the parent does not consume the data.
Long‑string test
When the child writes a large amount of data without the parent listening to the data event, the pipe buffer fills, the child blocks on printf, and the program appears to hang.
Removing the listener
Removing child.stdout.on('data') and not consuming the output causes the child to block, demonstrating the need to handle the pipe.
Debugging with GDB
We compile a simple C program child.c that prints repeatedly, spawn it from Node.js, and attach GDB to the child. The backtrace shows the process stuck inside printf, confirming that the write is blocked by a full pipe buffer.
Why the Stream Stops Reading
Node.js readable streams start in paused mode. Adding a 'data' listener switches the stream to flowing mode (internally calling resume()). In paused mode, data is buffered up to the high‑water mark (default 16384). If the buffer exceeds this limit and no 'data' handler is attached, the stream stops reading, causing the pipe to fill.
All Readable streams begin in paused mode but can be switched to flowing mode by adding a 'data' event handler, calling stream.resume() , or piping to a writable.
Unix Domain Socket Buffer
When options.stdio is set to 'pipe', Node.js actually creates a Unix Domain Socket with a default buffer of 65536 bytes. If the child writes more than this without the parent consuming it, the child blocks.
Summary
When you spawn a child process with stdio: 'pipe', you must consume its output—either by attaching a data handler or by explicitly ignoring the stream with { stdio: 'ignore' }. Otherwise the pipe buffer will fill, the child’s write calls will block, and the program will hang.
If you do not need the output, set stdio: 'ignore' to redirect the streams to /dev/null and avoid the blockage.
For deeper discussion, contact the author at kaidi.zkd#alibaba-inc.com.
Node Underground
No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.
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.
