Skip to content

Respawn loop has no cross-cycle circuit breaker — one crash-looping stdio server melts the host (load avg 143) #2

Description

@doobidoo

Summary

A single misconfigured stdio MCP server can drive the host into a process-spawn meltdown. On my Mac mini this produced load average 143 with ~76 concurrent child processes and file-descriptor exhaustion (Too many open files (os error 24)).

Environment

  • mcplex v0.3.0, macOS (Apple Silicon), launchd-managed (com.mcplex.gateway.local)
  • Affected backend: a telegram stdio server (mcp-telegram start) that was never configured (missing api_id/api_hash, no session file)

Root cause

dead_server_monitor in src/main.rs respawns dead stdio servers with exponential backoff, capped at MAX_RESPAWN_ATTEMPTS = 5. That cap only protects against connect() failing repeatedly within one respawn task.

It does NOT protect against the crash-after-successful-connect loop:

  1. StdioConnection::connect() succeeds — the child process spawns and completes the MCP handshake.
  2. The child crashes seconds later (in our case mcp-telegram raises RuntimeError: Client not created! inside app_lifespan because no Telegram credentials exist).
  3. The watchdog emits a fresh death event on death_rx.
  4. dead_server_monitor receives it and tokio::spawns another respawn task — with its own fresh 1..=5 counter.
  5. Goto 1, forever.

Because each crash also produces a fresh respawn task, and connect() partially succeeds (the wrapper with-secrets.shage → python child has already forked) before some attempts fail, processes pile up faster than they exit → FD exhaustion → os error 24 → host meltdown.

There is no memory of "this server has already died N times in the last minute" across respawn cycles, and concurrent death events can spawn concurrent respawn tasks for the same server.

Impact

One broken/misconfigured backend takes down the whole gateway host. Severity is high because the failure mode is silent (mcplex log keeps writing Respawn attempt 1/5 cheerfully) and sustained (hours, until manually killed).

Suggested fix

Add a cross-cycle circuit breaker in dead_server_monitor:

  • Track death timestamps per server (local HashMap<String, Vec<Instant>> — the monitor while loop is single-consumer, no lock needed).
  • If a server dies more than MAX_CRASHES_IN_WINDOW times within CRASH_WINDOW, mark the circuit open: log an error and stop respawning that server entirely.
  • This bounds the blast radius: a crash-looping server is abandoned after a few seconds instead of running forever.

Optionally also: ensure only one in-flight respawn task per server, and kill any leaked child before respawn.

I have a patch implementing the circuit breaker and will open a PR.

Workaround

Comment out the offending [[servers]] block in the config and restart mcplex. Load dropped from 143 to 2.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions