Summary
The telegram plugin (plugins/telegram, v0.0.7) can end up with multiple orphaned bun server.ts poller processes running simultaneously, each competing for Telegram's single getUpdates consumer slot. In the observed case, ~12 stray processes accumulated and together consumed roughly 9 CPU cores continuously, causing sustained near-100% system CPU usage on the host machine.
Root cause
Telegram's Bot API allows only one long-polling consumer per bot token. When a Claude Code session ends abruptly (crash, terminal killed, etc.), its telegram plugin's server.ts grandchild process can survive as an orphan and keep holding that single polling slot.
Each subsequent Claude Code session then starts its own poller, which receives 409 Conflict from Telegram because the orphan still holds the slot. In the version that caused this, the error handling around bot.start() / polling retries did not:
- reliably detect and terminate stale/orphaned poller processes on startup, and
- back off properly between retries, and/or
- give up after a bounded number of failed attempts,
which let multiple competing pollers accumulate over time (across many sessions) instead of being cleaned up, with each one retrying aggressively. The aggregate effect across a dozen or so accumulated zombie processes was substantial, sustained CPU load (~9 cores) on the host.
Suggested fix (already applied locally, works)
The following changes to server.ts resolved the issue and have been running cleanly since:
-
Stale-poller eviction on startup: before starting to poll, read a PID file (e.g.
~/.claude/channels/telegram/bot.pid), check whether that PID is alive and whether its command line actually still looks likeserver.ts(to avoid acting on a recycled PID), and if so send itSIGTERMbefore starting a new poller. Write the new process's own PID to the file afterward. -
A global
bot.catch()handler: without this, grammY's default error handler callsbot.stop()and rethrows on any unhandled error inside a message handler, silently stopping polling forever while the process stays alive (because MCP stdio keeps it running). Installingbot.catch()logs the error and lets polling continue. -
Proper retry loop with backoff and a cap, replacing a retry path that previously only handled
409and let any other error (ETIMEDOUT,ECONNRESET, DNS failures, etc.) propagate out of the retry loop permanently:- Retry on any polling error, not just 409.
- Exponential-ish backoff between attempts, capped (e.g. at 15s).
- After a bounded number of consecutive
409 Conflictresponses (e.g. 8), give up and exit cleanly with a clear log message ("another poller is holding the bot token (stray 'bun server.ts' process or a second session)"), instead of retrying forever.
Impact
On a machine running several Claude Code sessions over time (including ones that end abruptly), these zombie pollers can accumulate silently in the background. Because the parent MCP stdio connection to each dead session is itself gone, there's no in-app signal that these processes exist — the only visible symptom is overall system CPU staying unexpectedly high, which is confusing to diagnose without inspecting ps aux | grep bun / Activity Monitor.
Environment
- Plugin:
telegram@claude-plugins-official, version0.0.7 - Install path pattern:
~/.claude/plugins/cache/claude-plugins-official/telegram/0.0.7/server.ts - OS: macOS (Darwin)
- Observed: ~12
bun server.tsprocesses simultaneously, ~75% CPU each, sustained over hours (based on accumulated CPU time in Activity Monitor)
Happy to share the patched server.ts or open a PR with the above changes if useful.
Activity
0No activity yet.