Skip to content

process.ppid is cached at startup, never updates after parent death (differs from Node.js) #29169

@ropelletier

Description

@ropelletier

Summary

process.ppid in bun returns a cached value from startup and never updates, even after the parent process dies and the child is reparented to PID 1 (init). In Node.js, process.ppid is a live getter that reflects the current parent. This difference breaks orphan detection patterns that are common in process supervisors and MCP plugins.

Reproduction

# Terminal 1: Start a parent that spawns bun, then kill the parent
bash -c 'bun -e "
  setInterval(() => {
    const fs = require(\"fs\");
    const realPpid = parseInt(fs.readFileSync(\"/proc/\" + process.pid + \"/stat\", \"utf8\").split(\" \")[3]);
    console.log(\"process.ppid=\" + process.ppid + \" real_ppid=\" + realPpid);
  }, 2000);
" &
BUN_PID=$!
echo "Parent shell PID=$$, bun PID=$BUN_PID"
sleep 3
echo "Killing parent shell now..."
kill $$'

# After the parent shell dies, observe bun's output:
# process.ppid=<DEAD_PID>  real_ppid=1
# process.ppid=<DEAD_PID>  real_ppid=1
# ...
# process.ppid never changes to 1, even though /proc confirms reparenting

Expected behavior (matches Node.js)

After the parent dies:

process.ppid=1  real_ppid=1

Actual behavior (bun 1.3.12)

After the parent dies:

process.ppid=<original_dead_pid>  real_ppid=1

process.ppid is frozen at the value from process startup and never updates.

Environment

  • bun: 1.3.12 (also confirmed on 1.3.11)
  • OS: Ubuntu 24.04, kernel 6.8.0-107-generic x86_64
  • Node.js comparison: Node 20+ correctly returns updated ppid

Impact

This breaks any orphan-detection logic that relies on process.ppid === 1 to determine if the parent has died. Common pattern:

// Works in Node.js, broken in bun:
setInterval(() => {
  if (process.ppid === 1) {
    console.log('Parent died, shutting down gracefully');
    process.exit(0);
  }
}, 5000);

Real-world impact: The Claude Code Telegram MCP plugin uses this pattern for zombie process detection. Because bun never updates process.ppid, orphaned plugin processes persist indefinitely, holding resources (in this case, a Telegram Bot API long-poll that causes 409 Conflict for all subsequent instances).

Workaround

Read /proc/<pid>/stat directly to get the real ppid from the kernel:

import { readFileSync } from 'fs';

function getRealPpid(pid: number): number {
  try {
    const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
    return parseInt(stat.split(' ')[3], 10);
  } catch {
    return -1;
  }
}

This works but is Linux-only and shouldn't be necessary — process.ppid should just work.

Suggested Fix

process.ppid should be implemented as a getter that calls getppid(2) on each access (same as Node.js), rather than caching the value at startup.

Node.js implementation for reference: https://github.com/nodejs/node/blob/main/src/node_process_methods.cc (uses uv_os_getppid() which calls getppid(2))

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions