One day I was spawning a CLI from Node (the Claude CLI, which dumps a lot of JSON) and piping its stdout back to parse. Short outputs were fine. But the moment output grew past a few hundred KB, the last few KB just disappeared β JSON.parse blew up on the final line, and the truncation point shifted run to run.
After digging through Node’s official issues, Linux pipe docs, and community deep-dives, the verdict is blunt: this is a known Node behavior since 2015, and the only reliable pure-stdlib fix is writing to a temp file. This post lays out the trade-offs across six approaches so you don’t have to repeat the journey.
The Symptom
| |
The bigger the output, the higher the chance. MB-scale almost always truncates; hundreds of KB occasionally. The cut-off point isn’t fixed β sometimes you get 1.2 MB, sometimes 1.18 MB.
Why It Truncates
The root cause is how Node writes stdio. The child’s stdout connects to a pipe, and writes to a pipe are async. When the child calls process.exit(), Node doesn’t wait for buffered data to flush β the process exits immediately, and whatever sat in the pipe unread gets lost.
If stdout is a TTY or a regular file, writes are sync and this never happens. The bug only triggers on “non-TTY, non-file fds” β pipes, FIFOs, and sockets.
This was first tracked in Node issue #3669 (2015), then revisited in #6379 and #9633. The community consensus: user-land’s only reliable workaround is writing to a file. Core has no plans to change it.
Six Approaches Side by Side
| Approach | Viability | Trade-off |
|---|---|---|
| A. Temp file | Pure stdlib | One extra disk I/O, ~10ms |
| B. node-pty / get-pty-output | Child sees a TTY | Needs native build; CLI may inject ANSI codes that pollute JSON |
C. F_SETPIPE_SZ to enlarge pipe | Linux only | macOS lacks the API; only delays the cut-off point |
| D. Named pipe (FIFO) | Same dead end | FIFOs are non-file fds too β same truncation |
| E. UNIX socket | Same dead end | Sockets are also non-file fds, async writes still truncate |
| F. Fix the child CLI itself | Root cause | Usually not under your control |
Why D / E Don’t Work Either
A common first thought: “Skip the pipe, use a FIFO or UNIX socket β that should work, right?” I tried it. Same truncation.
The reason: “async pipe writes” isn’t a property of pipes specifically β it’s a property of non-file, non-TTY fds. Linux and macOS route writes to such fds through the async path, and FIFOs and sockets fall in the same bucket. Identical behavior.
Why C Looks Promising but Isn’t
fcntl(F_SETPIPE_SZ) can grow the Linux pipe buffer from 64 KB default to 1 MB (more than that needs root). Sounds great β fill the buffer big enough and nothing gets cut, right?
Three problems:
- Linux only. macOS doesn’t have
F_SETPIPE_SZ - Only delays the cut-off. Outputs > 1 MB still truncate β no real fix
- Still needs a native binding.
fcntlisn’t exposed in Node β you’d write a C++ addon or useffi-napi
If you want pure stdlib and cross-platform, this path is out.
What Actually Works: Pipe stdout to a File
The trick is to use fs.openSync to grab a file fd and hand it to spawn’s stdio option. This way the child writes stdout directly to the file, bypassing any pipe β writes are sync, process.exit() won’t truncate:
| |
For an async version, use spawn + child.on('close', ...). Same principle: fd to a file, never a pipe.
If stderr is also high-volume, do the same with a second fd.
'inherit'forwards to the parent’s stderr cleanly but you can’t capture it.
The ~10ms Disk I/O Cost
The only downside is the extra disk I/O β about 10ms on SSD. For a CLI that runs for several seconds, this is noise. If you genuinely care about that 10ms, the only remaining path is node-pty, but you’ll deal with:
- Native build (extra compile step in CI)
- Child sees a TTY and may inject ANSI color codes into stdout β strip them
- macOS and Windows backends differ (forkpty vs conpty), test both
My take: if a temp file works, use a temp file. Trading 10ms to avoid a native dependency and ANSI pollution is an easy win.
References
- Node.js issue #3669 β process.stdout/.stderr may lose data on process.exit()
- Node.js issue #6379 β stdout/stderr buffering considerations
- Node.js issue #9633 β Output data lost from spawned process if process ends before all data read
- pipe(7) β Linux manual page
- microsoft/node-pty
- Node.js child_process documentation
