
Race condition in StreamableHTTP: zero-buffer memory streams cause deadlock with concurrent SSE responses
快速结论:当使用 StreamableHTTP 传输(stateless 或 stateful 模式)且响应中包含 3 个及以上条目(或大量工具列表)时,SSE 连接可能永久挂起。优先排查 mcp/server/streamable_http.py 中 create_memory_object_stream(0) 缓冲区大小,增大缓冲区可解决。
问题场景
此问题出现在使用 MCP Python SDK 的 StreamableHTTPServerTransport 时,常见场景包括:
- 使用 FastMCP 构建的 MCP 服务器(stateless_http=True)返回包含数组(3+ 元素)的工具响应
- Claude Code 等客户端通过互联网连接 MCP 服务器(网络延迟可能加剧问题)
- stateful 模式下(使用 Mcp-Session-Id)发送大量工具列表请求(如返回 73 个工具的 mcp-atlassian 服务器)
- 客户端中途 TCP 中止 SSE 流后,后续请求永久挂起,仅心跳(: ping)正常
报错原文
SSE connections hang indefinitely when using StreamableHTTPServerTransport in stateless mode with responses containing 3+ items.
Expected: All tool responses should complete regardless of response size.
Actual: 1-2 items: Response returns immediately (~150ms). 3+ items: Request hangs indefinitely (deadlock).
HTTP 200, Content-Type: text/event-stream, Transfer-Encoding: chunked, 0 bytes of body, just `: ping` heartbeats.
/healthz keeps returning 200, so docker / k8s liveness probes don't recover the container.
原因分析
根本原因在 mcp/server/streamable_http.py 中使用了零缓冲区的内存流:
第 412 行:self._request_streams[request_id] = anyio.create_memory_object_stream[EventMessage](0)
第 460 行:sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](0)
零缓冲区意味着 send() 会阻塞直到 receive() 被调用。竞态条件流程如下:
- 通过
tg.start_soon(response, ...)启动 SSE 响应任务(非阻塞) - 主循环调用
await writer.send(session_message)将请求发送给 MCP 服务器 - MCP 服务器处理速度较快,调用
message_router并尝试await request_streams[id][0].send(EventMessage(...)) - 如果此时 SSE writer 任务尚未开始迭代(即尚未调用
receive()),send()会永久阻塞
小响应(1-2 项)正常工作是因为 SSE writer 任务在 MCP 响应到达前已启动;而大响应(3+ 项)中 MCP 处理更快,响应在 SSE 迭代器准备就绪前到达,导致死锁。
另一种可能原因(推测):src/mcp/shared/session.py 中 request_id 使用自增方式,在高并发或客户端中断后可能出现重复 ID,加剧竞态条件。
环境排查
- MCP Python SDK 版本:1.23.3 及以上(包括 1.26.0)
- Python 版本:3.11
- FastMCP 版本(如使用):2.13.1(但问题出在官方 SDK 的
streamable_http.py,非 FastMCP 代码) - 传输模式:StreamableHTTP(stateless_http=True 或 stateful 模式均可能触发)
- 网络环境:互联网连接(可能加剧)或局域网连接(未解决根本问题)
- 客户端:Claude Code、Cursor、自定义 httpx 脚本等
解决步骤
- 增大缓冲区大小(已验证方案):将
mcp/server/streamable_http.py中所有create_memory_object_stream(0)改为create_memory_object_stream(10)。具体涉及两处:- 第 412 行:
self._request_streams[request_id] = anyio.create_memory_object_stream[EventMessage](10) - 第 460 行:
sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](10)
- 第 412 行:
- Docker/生产环境快捷修复(sed 方案):可在 Dockerfile 中添加:
RUN sed -i 's/create_memory_object_stream\[EventMessage\](0)/create_memory_object_stream[EventMessage](10)/g' \ /usr/local/lib/python3.11/site-packages/mcp/server/streamable_http.py && \ sed -i 's/create_memory_object_stream\[dict\[str, str\]\](0)/create_memory_object_stream[dict[str, str]](10)/g' \ /usr/local/lib/python3.11/site-packages/mcp/server/streamable_http.py - 替代方案(可优先尝试):改用
await tg.start()替换tg.start_soon()来启动 SSE 响应任务,确保 SSE writer 在发送请求前完全就绪(需要EventSourceResponse支持任务状态协议)。 - 注意 stateless 和 stateful 路径的区别:PR #2145 修复了 stateless 模式下的任务累积问题,但未触及 stateful 模式下的缓冲区大小。如果使用 stateful 模式(Mcp-Session-Id),仅应用 #2145 的修补可能不够,仍需修改缓冲区大小。
验证方法
修复后,使用包含 3 个或以上元素的数组响应的工具进行测试:
- 对于 stateless 模式:调用返回数组的工具,确认所有响应完整返回(不应挂起)
- 对于 stateful 模式:模拟客户端中断后发送新请求,确认新请求正常返回(HTTP 200 含完整 SSE body)
- 在生产环境:监控文件描述符(FD)数量,确认无持续增长;检查 liveness probe(/healthz)是否正常
验证标准:所有工具响应应在几百毫秒内完成,无永久挂起,无仅发送心跳的 HTTP 200 响应。



