[plugin: telegram] server.ts spins at 100% CPU when multiple Claude Code sessions run concurrently — PID_FILE assumes single writer
Summary
Plugin claude-plugins-official/telegram@0.0.6 leaks bun server.ts processes that pin a CPU core at 100% indefinitely. After 10 days uptime with multiple concurrent Claude Code sessions, I had 3 orphaned bun server.ts instances each consuming ~100% CPU, driving system load average to 84 and triggering macOS kernel_task thermal throttling.
Environment
- Claude Code: 2.1.153
- bun: 1.3.14
- macOS Darwin 25.3.0 (Apple Silicon)
- Plugin:
claude-plugins-official/telegram0.0.6
Root cause
server.ts tries to mitigate Telegram's "one getUpdates consumer per token" rule by writing its PID to ~/.claude/channels/telegram/bot.pid and SIGTERM-ing whatever PID it finds there at startup:
try {
const stale = parseInt(readFileSync(PID_FILE, 'utf8'), 10)
if (stale > 1 && stale !== process.pid) {
process.kill(stale, 0)
process.stderr.write(`telegram channel: replacing stale poller pid=${stale}\n`)
process.kill(stale, 'SIGTERM')
}
} catch {}
writeFileSync(PID_FILE, String(process.pid))
The file is a single slot. When N concurrent Claude Code sessions each spawn their own bun server.ts, the last writer wins the PID slot. The earlier processes:
- No longer have their PID recorded, so no future session will SIGTERM them.
- Keep calling
bot.start()(long-pollinggetUpdates), which returns HTTP409 Conflictbecause another instance now owns the slot. - grammy's default retry behavior spins the request, pinning a CPU core at ~100%.
The .in_use/ directory under the plugin cache had 95 entries (one per session that ever loaded the plugin in 10 days) while only the most recent session's PID was tracked.
Reproduction
- Open ≥2 concurrent Claude Code sessions that both load the telegram plugin
- Wait for both
bun server.tsinstances to start - Observe: the older one pins a CPU core at 100% on
getUpdates409 retries - Verify with
ps -Ao pid,pcpu,args | grep "bun.*server\.ts"
Suggested fixes
Pick one (in order of robustness):
flock/exclusive lock onbot.pid— second instance detects the lock and exits cleanly with a clear message. Only one poller per host ever runs.- Detect
409 Conflictin thegetUpdateshandler andprocess.exit(0)— current instance becomes orphan-safe; no longer relies on the next session's startup logic as the only safety net. - Append-only PID list + SIGTERM all stale on startup — keeps current architecture, just fixes the single-slot bug.
Workaround
pkill -9 -f "bun.*server\.ts"
Next /telegram invocation respawns one cleanly.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗