Bug: Telegram MCP Plugin - Orphaned Processes Cause CPU Exhaustion
Bug Report: Telegram MCP Plugin - Orphaned Processes Cause CPU Exhaustion
Summary
The Telegram MCP plugin spawns bun server.ts processes that do not terminate when Claude Code sessions end, leading to accumulation of orphaned processes that exhaust CPU resources.
Environment
- Plugin:
telegram@claude-plugins-officialv0.0.4 - OS: macOS Darwin 25.4.0
- Runtime: Bun
- Date: 2026-04-03
Problem Description
Observed Behavior
Over the course of approximately 10 hours of Claude Code usage, 27 instances of bun server.ts accumulated on the system:
$ ps aux | grep "bun server.ts" | grep -v grep | wc -l
27
$ ps aux | grep "bun server.ts" | grep -v grep | awk '{sum+=$3} END {print "Total CPU%:", sum}'
Total CPU%: 502.8
Each instance consumed approximately 18-22% CPU, totaling over 500% CPU usage and causing system-wide CPU exhaustion (99.9% CPU usage reported).
Timeline Evidence
PID PPID TTY STARTED
34325 34320 ttys006 Fri Apr 3 09:42:35 2026 # Morning sessions
44369 44362 ttys007 Fri Apr 3 09:55:46 2026
78656 78653 ttys006 Fri Apr 3 10:31:24 2026
...
19822 19818 ?? Fri Apr 3 14:13:39 2026 # Orphaned (parent died)
26065 26058 ?? Fri Apr 3 14:26:39 2026 # Orphaned
...
75526 75524 ttys002 Fri Apr 3 19:12:36 2026 # Evening sessions
Orphaned Processes
Processes with TTY = ?? have lost their parent terminal session but continue running indefinitely. The parent PIDs (19818, 26058, etc.) no longer exist.
Working Directory
All processes originate from the Telegram plugin directory:
/Users/kobig/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/telegram
Root Cause Analysis
Code Review
The server.ts implements graceful shutdown handlers (lines 619-632):
process.stdin.on('end', shutdown)
process.stdin.on('close', shutdown)
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
However, these handlers are not triggered in the following scenarios:
- Terminal session closes abruptly — When a terminal tab/window is closed, processes may not receive SIGTERM
- Claude Code crashes — Unexpected exit doesn't always send SIGTERM to child processes
- System sleep/wake — Processes may be left in undefined state
- Parent process killed without signal propagation —
pkillon parent without-Pflag
Missing Cleanup Mechanism
The plugin lacks:
- Pidfile management — No tracking of running instances
- Singleton enforcement — Multiple instances can run simultaneously
- Stale process detection — No check for zombie processes on startup
- Health check endpoint — No way to verify process liveness
Impact
- CPU exhaustion — System becomes unresponsive
- Battery drain — Continued CPU usage on laptops
- Fan noise — Thermal management kicks in
- Process table pollution — Accumulation over days/weeks
Suggested Fixes
1. Add Pidfile with Singleton Enforcement
const PID_FILE = join(STATE_DIR, 'server.pid')
function acquireLock(): void {
const existingPid = readPidFile()
if (existingPid && isProcessRunning(existingPid)) {
process.stderr.write(`telegram channel: another instance is running (PID ${existingPid}), exiting\n`)
process.exit(0)
}
writeFileSync(PID_FILE, String(process.pid))
}
function releaseLock(): void {
try { rmSync(PID_FILE, { force: true }) } catch {}
}
function isProcessRunning(pid: number): boolean {
try { process.kill(pid, 0); return true } catch { return false }
}
2. Add Stale Process Cleanup on Startup
function cleanupStaleProcesses(): void {
// Check for orphaned bun server.ts processes from this plugin
// Kill any process that:
// 1. Matches the plugin's working directory
// 2. Has a parent that no longer exists (PPID != current session)
}
3. Use Process Group for Clean Termination
// Spawn in a process group for easier cleanup
const server = spawn('bun', ['server.ts'], {
detached: true, // Create new process group
})
// On shutdown, kill entire group
process.kill(-server.pid, 'SIGTERM')
4. Add Heartbeat/Health Check
// Write heartbeat timestamp periodically
setInterval(() => {
writeFileSync(join(STATE_DIR, 'heartbeat'), String(Date.now()))
}, 30000)
// On startup, check if last heartbeat is stale (> 5 min old)
// If stale, previous instance is likely dead
Workaround for Users
Until this is fixed, users can clean up orphaned processes with:
# Kill all orphaned telegram server processes
pgrep -f "bun server.ts" | xargs kill -9
# Or add to ~/.zshrc as an alias
alias clean-telegram='pgrep -f "bun server.ts" | xargs kill -9 2>/dev/null; echo "Cleaned up zombie processes"'
Severity
High — Causes significant performance degradation and can make systems unusable, especially for users who run Claude Code frequently throughout the day.
Steps to Reproduce
- Start Claude Code with Telegram channel:
claude --channels plugin:telegram@claude-plugins-official - Use Claude Code normally
- Close terminal tab or quit Claude Code
- Repeat steps 1-3 multiple times throughout the day
- Observe accumulated
bun server.tsprocesses withps aux | grep "bun server.ts" - Monitor CPU usage climb to 100%
Additional Context
- 409 Conflict handling (lines 959-994) correctly handles the case where a new instance tries to start while another is running, but doesn't address the orphaned process problem
- The retry mechanism with backoff is good but doesn't help when processes become zombies
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗