telegram plugin: bun server.ts leaks at 100% CPU on every MCP disconnect

Resolved 💬 2 comments Opened Jun 25, 2026 by berikobama Closed Jun 29, 2026

Plugin / version

telegram@claude-plugins-official v0.0.6 (./external_plugins/telegram in the marketplace repo).

Summary

Every time the Telegram MCP server disconnects (e.g. when the parent Claude Code session ends or the MCP transport drops), the underlying bun server.ts process survives, gets reparented to systemd, and pins one CPU core at 100% indefinitely. Multiple disconnects in one host session compound — I observed three leaked instances consuming ~3 cores before noticing the load.

Existing mitigations in server.ts (lines 645–677) do not stop it:

  • process.stdin.on('end' / 'close') shutdown handlers don't fire.
  • SIGTERM is ignored — kill <pid> has no effect.
  • The orphan-watchdog setInterval(...).unref() at line 671 never fires either.

Only SIGKILL reclaims the process. The fact that both the signal handler and the watchdog setInterval are unresponsive strongly suggests the Node/Bun event loop is wedged by a synchronous busy-loop after the transport dies — likely a tight retry/error loop somewhere in the post-disconnect path.

Reproducer

  1. Install telegram@claude-plugins-official and pair a bot.
  2. Start a Claude Code session that activates the plugin.
  3. End the session (or trigger an MCP transport disconnect — in my case the host emitted plugin:telegram:telegram disconnected mid-session and the same leak occurred).
  4. ps -eo pid,ppid,pcpu,etime,cmd | grep "bun server.ts" — the orphaned process is pinned at ~100% CPU with parent PID 1 (or systemd-user).
  5. kill <pid> is ignored; only kill -9 works.

Reproduced 4× in a single host session on my machine (Arch, Linux 7.0.12, bun installed from the bun.sh script).

Suspected cause

shutdown() (line 649) hops off to bot.stop() and a setTimeout fallback to process.exit(0) — but both depend on the event loop progressing. If the disconnect path enters a synchronous loop (e.g. grammy reconnect loop, an unhandled promise that rejects synchronously in a tight cycle, or a while somewhere in the SDK transport), neither timer fires and signals queue forever.

Suggested fixes

  1. Find the busy-loop. Run with bun --inspect after a forced disconnect and capture a CPU profile from the wedged process; the hot frame will identify the offender. Most likely candidate is grammy's long-poll retry without backoff after stdin-EOF.
  2. Add an independent kill switch that doesn't depend on the main event loop — e.g. a Worker thread whose only job is setInterval(checkParent, 1000); if (orphaned) process.kill(process.pid, 'SIGKILL'). Workers have their own event loop, so they survive the main loop wedging.
  3. Backstop in the wrapper. The bun run wrapper script could trap on its own exit and kill -9 the child PID; right now nothing enforces termination once the wrapper dies.

Impact

Silent CPU drain — easy to miss on a workstation, but on a battery-powered laptop or any always-on dev box this burns through power budget. Also makes the plugin a noisy neighbor for any concurrent work.

Workaround for affected users

ps -eo pid,pcpu,etime,cmd --sort=-pcpu | grep "bun server.ts" | grep -v grep
# kill any leaked instance (i.e. one not owned by your current claude session):
kill -9 <pid>

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗