mirror of
https://github.com/open-webui/mcpo.git
synced 2026-11-05 01:27:39 +00:00
[GH-ISSUE #302] mcpo pins one CPU core at 100% indefinitely when an upstream streamable-HTTP MCP server returns a transient HTTP error (e.g. 502) mid-connection #135
Labels
No labels
bug
can't reproduce
documentation
enhancement
good first issue
help wanted
invalid
pull-request
question
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
open-webui/mcpo#135
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @jlacour-git on GitHub (Jul 15, 2026).
Original GitHub issue: https://github.com/open-webui/mcpo/issues/302
Summary
A single streamable-HTTP MCP upstream that returns a transient HTTP error (a
502 Bad Gatewayin ourcase) during an established connection causes mcpo to leave a broken async-generator teardown behind.
anyio raises
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in,and from that point one CPU core is pinned at ~100% indefinitely — the process never recovers on
its own. Only
podman restart mcpo(killing the process) clears it. We have observed it run ~14hours (== full host uptime) before being noticed via fan/thermal.
This is distinct from the existing reports:
neither cures this path.
startup. Ours is a runtime error on an already-established connection, not startup.
The trigger here is specifically a transient upstream HTTP error on a live connection.
Environment
0.0.20(imageghcr.io/open-webui/mcpo:main, digestsha256:05ccaa3bb0de51eaa9d00a96c4583dde93897c1b45812b34ed3de3b033f06fbf) — this is the current:mainamd64 build (verified against the live registry manifest 2026-07-15; not a stale local image)1.26.0, anyio4.12.1, httpx0.28.1, httpx-sse0.4.3, uvicorn0.41.0, starlette0.52.13.12.12, Linux (Podman container, rootless)streamable-httpupstream + one stdio upstream (mcp-server-fetch)Config (redacted)
Symptom signature
R,wchan=0— a busy loop, not workload.healthy; logs show no tool traffic while spinning.Actual traceback (from our logs)
The connection was live; the upstream returned
502 Bad Gatewaymid-request.raise_for_status()raised inside the streamable-HTTP client's TaskGroup; during teardown
GeneratorExitpropagatedthrough the client's async generator and anyio failed the cancel-scope exit:
Steps to reproduce
streamable-httpupstream.in front that returns
502for one request, or bounce the upstream so an in-flight request 502s).Expected
A transient upstream HTTP error should surface as a normal error for that request; the client should
tear down the failed connection cleanly (child task signals; the owning task closes its own cancel
scope) and mcpo should keep serving. It should never enter an unrecoverable busy-loop.
Root-cause note (for maintainers)
The failure is the anyio cancel-scope ownership rule being violated during streamable-HTTP teardown:
a cancel scope entered by the supervisor task is exited from the child (reader) task's context via a
shared stream/generator
aclose(). anyio can only detect this; the fix has to be in the client'sconnection lifecycle (child signals, parent closes its own scope), and it needs to be robust to the
teardown being triggered by an in-flight
HTTPStatusError, not just clean shutdown.Workaround we deployed
A host watchdog that detects the sustained single-core pin (Δ CPU jiffies ≈ one full core sustained
over several minutes) and restarts mcpo + alerts. Effective but obviously a band-aid.
@giuliocapecchi-tuboolar commented on GitHub (Sep 11, 2026):
Same symptom here (mcpo 0.0.20, mcp 1.26.0, anyio 4.12.1), different trigger: a
streamable-httpbackend whose port is closed when mcpo starts. One core at 100% until the container is restarted; we have seen it run 11 hours. Reproduces in about 60 seconds, with and without--hot-reload.The RuntimeError is the MCP SDK's problem, but the CPU spin itself is anyio, not mcpo: agronholm/anyio#1111, fixed in anyio 4.14.0.
CancelScope._deliver_cancellationsetsshould_retry = Truefor a task that is already done,task.cancel()is a no-op on it, so the callback reschedules itself viacall_soonon every loop iteration.py-spy dump --localson the spinning process shows exactly that:Verified fix, same image and same trigger:
CPU goes from 100% to 0.1% over 5 minutes. The RuntimeError still appears in the logs, and a backend that was down at startup still needs a restart to be picked up, but the process no longer burns a core. An
anyio>=4.14pin in mcpo's dependencies would fix this for everyone.