Skip to content

stdout truncated at 64KB/128KB/256KB boundary when using saved process.stdout.write after override #25432

@EdAyers

Description

@EdAyers

What version of Bun is running?

1.3.4+5eb2145b3

What platform is your computer?

Darwin 25.1.0 arm64 arm

What steps can reproduce the bug?

Create a TypeScript file that overrides process.stdout.write and later writes to the original stdout:

minimal_trigger.ts:

// Save original stdout.write
const originalStdoutWrite = process.stdout.write.bind(process.stdout);

// Override process.stdout.write (e.g., to redirect logs to stderr)
process.stdout.write = function(chunk: any, ...args: any[]): boolean {
  return process.stderr.write(chunk, ...args);
};

// Read stdin (works in both Bun and Node)
async function readStdin(): Promise<string> {
  if (typeof (globalThis as any).Bun !== 'undefined') {
    return await (globalThis as any).Bun.stdin.text();
  }
  return new Promise((resolve) => {
    let data = '';
    process.stdin.on('data', (chunk) => { data += chunk; });
    process.stdin.on('end', () => resolve(data));
  });
}

const stdin = await readStdin();

// Generate large output (~216KB)
const output = JSON.stringify({
  items: Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    description: `Description for item ${i} with extra text to increase size.`,
    metadata: { index: i, category: `Cat ${i % 10}` }
  }))
}, null, 2);

// Write using original stdout (bypassing override)
originalStdoutWrite(output);
originalStdoutWrite('\n');

// Call process.stdout.end() before exit
process.stdout.end(() => {
  process.exit(0);
});

Test script (Python):

import asyncio

async def test_runtime(runtime: str):
    for i in range(5):
        proc = await asyncio.create_subprocess_exec(
            runtime, 'minimal_trigger.ts',
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, _ = await proc.communicate(b'')
        print(f'{runtime}: {len(stdout)} bytes')

async def main():
    await test_runtime('bun')
    await test_runtime('node')

asyncio.run(main())

What is the expected behavior?

Both Bun and Node.js should output the complete JSON data (~216KB):

What do you see instead?

Bun truncates output at exactly 65536 bytes (64KB):

bun: 65536 bytes
bun: 65536 bytes
bun: 65536 bytes
node: 216580 bytes
node: 216580 bytes
node: 216580 bytes

The output is cut off at exactly the 64KB boundary. Node.js outputs the full 216KB correctly.

Additional information

Environment:

  • macOS 26.1 (arm64)
  • Bun 1.3.4
  • Node.js v24.4.1
  • Python 3.13.8

Key findings:

  1. Truncation occurs at power-of-2 boundaries - we observed truncation at multiple points depending on the exact code:
    • 64KB (65536 bytes) - most common with the minimal reproduction
    • 128KB (131072 bytes) - observed with larger/more complex bundles
    • 192KB (196608 bytes) - observed intermittently
    • 256KB (262144 bytes) - observed with more stderr output
  2. Node.js works correctly with the same code in all cases

The bug is triggered by this specific combination:

  1. Save reference to original process.stdout.write using .bind()
  2. Override process.stdout.write to redirect output elsewhere (e.g., to stderr)
  3. Later call the saved original stdout.write to write final output
  4. Call process.stdout.end() before process exit

Variations we tested:

  • With/without stdin reading - truncation occurs either way
  • With/without stderr output during processing - affects truncation point
  • Simple scripts vs bundled applications - bundled apps sometimes truncate at higher boundaries (128KB-256KB)
  • The truncation point can vary between runs of the same code, always landing on a power-of-2 boundary

Use case: This pattern is common in CLI tools that want to:

  • Redirect logs/debug output to stderr during processing
  • Send only structured data (e.g., JSON) to stdout as final output
  • Properly flush and close output streams before exit
  • Our particular use case was with Langium, which produces a lot of logging noise to stdout that we want to suppress.

This bug was found with a lot of help of Claude Code but I, a human, have verified this report is correct.

This is likely what was being observed in #20562 and #7638, I've made a new issue because that one got closed and I can't reopen.

Possibly this is just a legit race condition with process closing and having stuff in an stdout buffer. In which case if you could tell me what the correct way of supressing stdout from libraries is I'd love to know.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions