Skip to content

mcp.server.fastmcp.FastMCP HTTP transport deadlocks after ~5 sequential client sessions on Windows (companion to PrefectHQ/fastmcp#4192) #2653

Open

Description

@colinlabrooy

mcp.server.fastmcp.FastMCP HTTP transport deadlocks after ~5 sequential client sessions on Windows (companion to PrefectHQ/fastmcp#4192)

Summary

Filing here as a companion to PrefectHQ/fastmcp#4192, which contains the full investigation, cross-control matrix, stack-dump evidence, and minimal reproducers. The reason for the companion issue: the bug reproduces on Anthropic's mcp.server.fastmcp.FastMCP shipped inside this SDK, faster than on PrefectHQ's downstream fork, and the SDK maintainers should see the signal directly rather than having to chase a downstream cross-reference.

The one-line result that's yours

A 30-sequential-session driver running against an HTTP server built with mcp.server.fastmcp.FastMCP 1.27.1 on Windows + Python 3.14 hangs at session #5, with gradual slowdown visible from session #4 onwards. The same driver against PrefectHQ fastmcp.FastMCP 3.3.1 hangs at session #12; against raw mcp.server.lowlevel.Server it passes 30/30; against the Node.js TypeScript reference server (@modelcontextprotocol/server-everything) it passes 30/30.

That is: this SDK's FastMCP wrapper exhibits the defect more aggressively than the downstream fork, and the SDK's own lower-level surface is unaffected.

Cross-control matrix (excerpt; full matrix in PrefectHQ/fastmcp#4192)

Server Python Sessions before hang
mcp.server.fastmcp.FastMCP 1.27.1 3.14 5
mcp.server.fastmcp.FastMCP 1.27.1 3.13.7 12
fastmcp.FastMCP 3.3.1 (PrefectHQ) 3.14 12
mcp.server.lowlevel.Server + StreamableHTTPSessionManager 3.14 30/30 ✅
@modelcontextprotocol/server-everything (Node.js streamableHttp) n/a 30/30 ✅

Python-version variance rules out a 3.14 regression. The Node.js control rules out a protocol-level defect. The raw-SDK control rules out the SDK's lower-level transport.

Mechanism (per stack-dump evidence in #4192)

python -m asyncio ps <PID> on the hung server process shows multiple Task entries (one per "closed" session) parked in TaskGroup.__aexit__, all with child tasks named sse_starlette.sse.EventSourceResponse.__call__.<locals>.cancel_on_finish blocked in Event.wait / sleep on _ping, _listen_for_disconnect, _stream_response, and the SDK's standalone_sse_writer.

mcp.server.fastmcp.FastMCP.create_streamable_http_app instantiates EventSourceResponse from sse_starlette as part of its ASGI app construction. The raw mcp.server.lowlevel.Server path writes text/event-stream directly via its own ASGI write loop without ever instantiating EventSourceResponse — which is why it isn't affected. The defect is in the wrapper layer's interaction with sse_starlette, not the SDK's core transport.

Reproducers

Two minimal reproducers in the PrefectHQ issue: one for PrefectHQ FastMCP, one for raw SDK (the passing control). A third targeting mcp.server.fastmcp.FastMCP directly is trivially derivable — same shape, different import — and I'm happy to add it here if useful.

Why the cap is lower on this SDK's wrapper

Working hypothesis (not yet verified): mcp.server.fastmcp.FastMCP and PrefectHQ's fastmcp.FastMCP use slightly different sse_starlette integration shapes. Either version-pin differences, or an extra layer of session-lifecycle handling in one wrapper that creates more orphan tasks per session, would explain why the threshold lands at 5 here vs 12 downstream. Confirming this would need a side-by-side diff of the two wrappers' create_streamable_http_app paths and sse_starlette versions, which I haven't done.

Platform / environment

  • Windows 11 (ProactorEventLoop)
  • Python 3.14 and 3.13.7
  • mcp 1.27.1 (the SDK that ships mcp.server.fastmcp.FastMCP)
  • Linux not validated — PrefectHQ's public CI runs Linux + Python 3.12 where epoll-based loop semantics may drain orphan tasks faster. Maintainer Linux validation requested in #4192; same request stands here.

The question for SDK maintainers

Is mcp.server.fastmcp.FastMCP considered an actively-maintained surface that the SDK team will fix, or is it a thin reference wrapper where PrefectHQ's fork is the canonical production target? The answer changes where the fix should land and whether this issue stays open here or gets closed in favour of #4192.

If it's in-scope here, the cross-controls in #4192 already narrow the surface to "EventSourceResponse integration in the FastMCP wrapper layer." The raw SDK path is the working reference.

Workaround for users of this SDK

Migrate from mcp.server.fastmcp.FastMCP to mcp.server.lowlevel.Server + manually-constructed StreamableHTTPSessionManager + Starlette + uvicorn. ~1 day's mechanical rewrite for a server with ~7 tools. Empirically 30/30 sessions passing on the same workload.

Activity

  1. added
    triageQueued for automated analysis — bot will process and remove this label
    on May 31, 2026
  2. ghost added
    bugSomething isn't working
    on Jun 1, 2026
  3. ghost added
    needs reproneeds additional information to be able to reproduce bug
    on Jun 1, 2026
  4. ghost removed
    triageQueued for automated analysis — bot will process and remove this label
    on Jun 1, 2026
  5. colinlabrooy commented on Jun 10, 2026

    @colinlabrooy
    Author

    A small follow-up — a re-confirmation and a caution that may save others time.

    Re-confirmation: no repro at 30 sessions. On the same Windows 11 (26200) / Python 3.14 box, a fresh 30-cycle reconnect soak (sequential connect → call → disconnect against a Streamable-HTTP server) passed 30/30 with the standing GET stream live — consistent with the Linux 30/30 above and the earlier 110-session run, so the hang stays a Windows/Proactor timing heisenbug here. For completeness I also ran it with the standing GET stream retired (server returns 405 on GET, so _handle_get_request() / EventSourceResponse is never entered): also 30/30. With the baseline already green that isn't decisive on its own — it's just the result you'd expect if the standalone GET-stream teardown is the locus (one of the three EventSourceResponse sites noted above), and it lines up with stateless_http=True as the practical mitigation.

    Caution for anyone building a minimal reproduction. While writing the soak I hit a hang that looked exactly like the reported "~session #12" wall — and it turned out to be a confound in my own harness, not the server: I was capturing the server subprocess's stdout/stderr through an undrained subprocess.PIPE. uvicorn emits ~5 access-log lines per reconnect cycle, and around cycle ~12 that fills the OS pipe buffer (~64 KB on Windows); the server then blocks on its own log write and stops servicing requests — presenting identically to the deadlock under test. Draining the sink (temp file / DEVNULL / reader thread) made it disappear and the soak ran 30/30. I flag it only because a repro driver that pipes server output without draining will produce a false positive that mimics #12 — worth ruling out before attributing a new "#12 hang" to this issue. It does not explain away the original capture (the FastMCP companion, PrefectHQ/fastmcp#4192, has the asyncio task dump with non-pipe sinks).

    (Cross-posted to PrefectHQ/fastmcp#4192.)

  6. isheng-eqi commented on Jul 6, 2026

    @isheng-eqi

    I can reproduce this on Windows 11 + Python 3.13 — the deadlock at session #5 is consistent with the investigation in PrefectHQ/fastmcp#4192.

    I have experience debugging concurrency/deadlock issues in Python async code (previously fixed a cron double-execution race condition). Running on Windows natively means I can iterate on the fix without cross-OS guesswork.

    I'd like to work on this. Happy to start with a clean repro script and trace the deadlock through the HTTP transport layer. Let me know if I should go ahead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds reproneeds additional information to be able to reproduce bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions