Telegram plugin: zombie bun processes consume 100% CPU after session close
Status Closed — not planned
Maintainer reply None cached
Workaround ✓ Mentioned in thread ↓
Activity 8 comments · opened Apr 3, 2026 · closed May 28, 2026
Bug
When a Claude Code session ends without cleanly closing stdin (e.g. force quit, crash, or the stdin pipe not being torn down properly), the Telegram plugin's bun server.ts process becomes orphaned (PPID becomes 1) and enters an infinite 409-retry loop in bot.start(), consuming 100% CPU permanently. Multiple sessions produce multiple zombies — I had 3 orphaned processes each at 100% CPU (984 minutes of cumulative CPU time).
Root cause
Two issues in external_plugins/telegram/server.ts:
- Cleanup relies solely on stdin EOF (
process.stdin.on('end', shutdown)at line 629-630) — there is no fallback mechanism (PID lock file, parent-process heartbeat, etc.) to detect that the parent Claude Code process is gone.
- The 409 Conflict retry loop (line 960) has no max-attempt cap — when a zombie holds the polling token, new instances retry forever with only 15s max backoff. Meanwhile the zombie itself is also looping. All of them burn CPU indefinitely.
// line 959-988 — infinite loop, no exit condition
void (async () => {
for (let attempt = 1; ; attempt++) {
try {
await bot.start({ ... })
return
} catch (err) {
if (err instanceof GrammyError && err.error_code === 409) {
const delay = Math.min(1000 * attempt, 15000)
// retries forever...
await new Promise(r => setTimeout(r, delay))
continue
}
...
}
}
Suggested fix
- Add a max retry count (e.g. 10-20) to the 409 loop — exit the process after exhausting retries.
- Implement a PID-file lock or parent-process liveness check so orphaned processes can detect the parent is gone and self-terminate.
Environment
- macOS Darwin 25.4.0
- Telegram plugin version 0.0.4
- Bun runtime
8 Comments
+1 — confirmed on macOS Darwin 25.3.0, Telegram plugin v0.0.4. Accumulated 12+ orphaned
bun server.tsprocesses over 3 days (some since March 31), two pegging 65% CPU each. Plugin disabled as workaround. Processes requiredkill -9— regular SIGTERM was ignored.+1, reproducing on macOS 15.7.4 (Darwin 24.6.0), Claude Code 2.1.97, Telegram plugin v0.0.4, Bun 1.3.11.
A data point on how bad this gets in the wild — a user came to me convinced their Mac battery was failing (slow charging, fast drain). Real cause was load average ~9 from 6 stacked
bun server.tsprocesses, each pegged at 100% CPU:| PID | age | state |
|---|---|---|
| 13490 | 8d 3h | orphan |
| 19787 | 8d 3h | orphan |
| 47956 | 8d 1h | orphan |
| 2859 | 5d 22h | orphan |
| 57914 | 4d 1h | orphan |
| 66421 | 5h 27m | active (current session) |
Two observations worth adding to the existing root-cause analysis:
kill -9worked. That rules out "stuck on async I/O" and points at a blocked event loop (synchronous tight loop). The shutdown handler's 2-secondsetTimeoutforce-exit fallback at server.ts:626 won't fire under those conditions either, so adding a liveness-based fallback won't help unless the sync-loop root cause is also fixed.Workaround for anyone landing here: disable via
enabledPluginsin~/.claude/settings.json, thenkill -9 $(pgrep -f 'bun.*server\.ts').---
Posted by Claude (Opus 4.6) on behalf of @BVCampos, after diagnosing his machine in a Claude Code session. Process evidence and environment details were gathered directly from his system.
Reproducing on Linux (Ubuntu 22.04, kernel 6.8) — adds platform coverage. Same pattern: plugin versions 0.0.5 and 0.0.6 both affected. Zombies recur within ~2 hours of clearing — killed two orphans this morning and a new one (plugin 0.0.6) appeared 1h 45m later, same 100% CPU signature.
Forensic detail from a live zombie (PID 319988, 22 threads, State R):
SIGPIPE/EPIPEdelivered, and grammy's polling timers keep firing.timerfds + eventfd + eventpoll — classic busy event loop with no downstream work.Suggests the fix belongs at the MCP stdio shutdown path: detect peer-closed stdio and exit the bun process, rather than relying on grammy to notice via a network error that never comes.
Reproducing on Linux (Ubuntu 24.04.4 LTS, Node v24.13.0, Claude Code 2.1.116) — the current
platform:macoslabel is narrower than the actual footprint.Additional trigger beyond session close:
/reload-pluginsalso leaks. Every reload leaves the oldbun server.tsorphaned (PPID becomes 1), and multiple zombies compete for Telegram's long-pollinggetUpdates(the Bot API allows only one client at a time), causing 409 Conflict loops at ~100% CPU each. This also explains the observed symptom where Telegram MCP disconnects ~1–2 minutes after every/reload-plugins, then works briefly, then breaks again as zombies fight over the poll connection.Observed today on an otherwise-idle workstation after ~4
/reload-pluginscycles:SIGTERM is ignored (the retry loop traps it but yields rarely enough that the signal handler never runs) — SIGKILL cleans up.
Suggested fix: the plugin host should track MCP-server PIDs (or their process groups) and SIGKILL them before spawning replacements on
/reload-plugins, in addition to cleaning up on session close.+1, adding a new trigger and a PPID pattern that existing workarounds miss.
Env: macOS 15 (Darwin 24.6.0), Apple M4 Pro, Telegram plugin v0.0.6, Bun latest.
Trigger: Claude Code launched from a Cursor integrated terminal
Starting
claudeinside Cursor's integrated terminal and closing the tab (or reload-window) reliably produces zombies — 3 stacked at ~100% CPU for 6+ hours in my repro. The same Claude Code launched from Terminal.app / iTerm doesn't reproduce on normal close. Suggests Cursor doesn't cascade SIGHUP through the shell →bun runwrapper →bun server.tschain when tearing down an integrated terminal.PPID chain pattern — existing
ppid=1workarounds miss thisIn most reports,
bun server.tshasPPID=1directly. Here the chain is two-level:The
bun run --cwd ... --silent startwrapper survives orphaned with PPID=1, but its childserver.tskeeps the wrapper as direct PPID. So "kill where PPID=1" misses these — and so would a fixedprocess.ppid(per #46637), since the direct parent is literally alive. The watchdog needs to walk one level up the PPID chain.Suggested fix — portable orphan detection (drop-in for
server.ts:657-664)Works on macOS (no
/proc) viaps, covers both direct-orphan and wrapper-orphan cases:User-side hotfix handling the two-level chain
Extension of the
ppid=1script from #46637:Closing for now — inactive for too long. Please open a new issue if this is still relevant.
Filed #66309 independently: Diagnosed the root cause as the orphan watchdog checking process.ppid !== bootPpid, but missing the case where bun run (the direct parent) gets reparented to init when CC exits. server.ts's own ppid never changes, so the watchdog doesn't fire. Fix: read grandparent PID from /proc/<ppid>/status at startup and check for reparenting. Patch available in #66309.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.