Telegram plugin: zombie bun processes after Claude Code exits (~99% CPU each)
Problem
When Claude Code exits, the Telegram channel plugin (bun server.ts) keeps running as a zombie process consuming ~99% CPU. Each Claude Code session leaves behind an orphaned process, so they accumulate over time.
After a few days of normal use I had 4 zombie bun server.ts processes, each at 99% CPU and thousands of minutes of CPU time accumulated:
PID %CPU STARTED TIME COMMAND
26630 99.0 Wed11PM 2010:32.87 /Users/.../.bun/bin/bun server.ts
63451 99.0 Thu11PM 2010:16.14 /Users/.../.bun/bin/bun server.ts
50757 99.4 8:16PM 877:31.59 /Users/.../.bun/bin/bun server.ts
85726 98.9 Sun07PM 8:07.69 /Users/.../.bun/bin/bun server.ts
Root cause
Process chain: Claude Code → bun run (wrapper) → bun server.ts
server.ts has a shutdown handler on process.stdin.on('end', shutdown) (line 629), which should fire when Claude Code closes the MCP connection. However:
- When Claude Code exits, the intermediate
bun runwrapper gets reparented to PID 1 (launchd) but stays alive - Because
bun runstays alive, it keeps stdin open for the childbun server.ts - The
stdin.on('end')handler never fires bun server.tsenters an infinite 409 Conflict retry loop (another instance already has the Telegram polling slot), burning CPU forever
Evidence — all parent bun run wrappers had PPID=1:
PPID PID COMMAND
1 26543 bun run --cwd ...telegram/0.0.4 --shell=bun --silent start
1 50502 bun run --cwd ...telegram/0.0.4 --shell=bun --silent start
1 63435 bun run --cwd ...telegram/0.0.4 --shell=bun --silent start
1 85721 bun run --cwd ...telegram/0.0.4 --shell=bun --silent start
Suggested fix
Add an orphan watchdog to server.ts that periodically checks if the grandparent (Claude Code) is still alive. If the parent's ppid becomes 1, it means Claude Code exited and the process should shut down:
// After the existing shutdown handlers (line 632):
if (process.platform !== 'win32') {
const { spawnSync } = await import('child_process')
const bootPpid = process.ppid
const orphanCheck = setInterval(() => {
try {
process.kill(bootPpid, 0)
const result = spawnSync('ps', ['-o', 'ppid=', '-p', String(bootPpid)])
const grandPpid = result.stdout.toString().trim()
if (grandPpid === '1') {
process.stderr.write('telegram channel: orphan detected (grandparent died), shutting down\n')
clearInterval(orphanCheck)
shutdown()
}
} catch {
process.stderr.write('telegram channel: parent process gone, shutting down\n')
clearInterval(orphanCheck)
shutdown()
}
}, 30_000)
orphanCheck.unref()
}
Alternatively, the root cause could be fixed in Claude Code itself — by ensuring it sends SIGTERM to MCP server process groups (not just the direct child) on exit, or by using a process group so the entire tree gets cleaned up.
Environment
- macOS 14.7 (Darwin 23.6.0)
- Telegram plugin version: 0.0.4
- Bun 1.x (from
~/.bun/bin/bun)
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗