Skip to content

bug: parallel cf-api execute processes spin at ~100% CPU after caller exits #16

Description

@ethan-huo

Summary

Running several mcpx cf-api execute invocations concurrently can leave Bun processes alive after the caller has completed. Each orphaned process spins at roughly one full CPU core indefinitely.

Observed incident

Environment:

  • mcpx: 0.9.15
  • repository revision: b297d683a33235872b79a4929896f5e16974da53
  • Bun: 1.3.14
  • macOS: 26.5.2 (25F84), arm64
  • server: cf-api

Five independent zone queries were launched concurrently. Four bun .../mcpx cf-api execute children remained alive after the orchestration call had returned. More than two hours later each process still consumed 96–97% CPU:

PID    %CPU  ELAPSED   STATE
34999  96.6  02:23:22  Rs
35000  96.4  02:23:22  Rs
35875  96.3  02:20:49  Rs
35877  96.3  02:20:49  Rs

Two surviving processes used structured --input; two used the equivalent explicit form:

mcpx cf-api execute --raw \
  --account_id ACCOUNT_ID \
  --code 'async () => { const r = await cloudflare.request({ method: "GET", path: "/zones", query: { name: "usemarkd.app", "account.id": accountId } }); return { success: r.success, zones: r.result }; }'

The same request executed sequentially completed normally in about one second. This points to a concurrent lifecycle/cleanup race rather than a permanently pending Cloudflare request.

All four spinning processes exited immediately after SIGTERM; SIGKILL was not required.

Expected behavior

Every mcpx ... execute invocation must terminate after producing its result or error, including when multiple CLI processes run concurrently. No timer, transport, stdio handle, worker, or cleanup loop should keep the Bun process alive or busy-spin after completion.

Suggested regression coverage

Add a process-level test that launches several execute commands concurrently against a deterministic test MCP server, waits for all outputs, and asserts:

  • every child exits within a bounded deadline;
  • exit codes and outputs are preserved;
  • no child remains alive after the parent/orchestrator closes its pipes;
  • error and cancellation paths also release transports, timers, workers, and signal handlers.

A CPU-spin detector or bounded post-result process-exit assertion would expose this class of leak directly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions