Initial Checks
Description
Bug Description
SSE connections hang indefinitely when using StreamableHTTPServerTransport in stateless mode with responses containing 3+ items. The issue is caused by zero-buffer memory streams that block send() until receive() is called, creating a race condition between the response writer and the SSE stream iterator.
Related issues: #262 describes similar symptoms (client hangs on call_tool()) but root cause wasn't identified. This issue provides the specific cause and fix.
Expected Behavior
All tool responses should complete regardless of response size.
Actual Behavior
- 1-2 items: Response returns immediately (~150ms)
- 3+ items: Request hangs indefinitely (deadlock)
Root Cause Analysis
The issue is in mcp/server/streamable_http.py:
Line 412 - zero-buffer request stream
self._request_streams[request_id] = anyio.create_memory_object_streamEventMessage
Line 460 - zero-buffer SSE stream
sse_stream_writer, sse_stream_reader = anyio.create_memory_object_streamdict[str, str]
Race condition flow:
- tg.start_soon(response, ...) starts SSE response task (non-blocking)
- await writer.send(session_message) sends request to MCP server
- MCP server processes quickly and calls message_router
- message_router tries await request_streams[id][0].send(EventMessage(...))
- DEADLOCK: If SSE writer hasn't started iterating yet, send() blocks forever
With zero-buffer streams, send() blocks until the receiver calls receive(). When the MCP server processes faster than the SSE writer task starts, deadlock occurs.
Why timing matters:
- Small responses (1-2 items): SSE writer task starts before MCP response arrives → works
- Larger responses (3+ items): MCP processes faster → response arrives before SSE iterator starts → blocked forever
Proposed Fix
Increase buffer size from 0 to a reasonable value (e.g., 10 or 100):
Line 412
self._request_streams[request_id] = anyio.create_memory_object_streamEventMessage
Line 460
sse_stream_writer, sse_stream_reader = anyio.create_memory_object_streamdict[str, str]
Alternative fix: Use await tg.start() instead of tg.start_soon() to ensure SSE writer is ready before sending requests (requires EventSourceResponse to support task status protocol).
Workaround
We've applied this fix via sed patch in our Dockerfile:
RUN sed -i 's/create_memory_object_stream[EventMessage](0)/create_memory_object_streamEventMessage/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_streamdict[str, str]/g'
/usr/local/lib/python3.11/site-packages/mcp/server/streamable_http.py
This resolves the issue in our production environment.
Example Code
from fastmcp import FastMCP
import json
mcp = FastMCP("test-server")
@mcp.tool()
async def test_tool() -> str:
# Returns JSON with 3+ items - will hang
return json.dumps({
"results": [{"n": "a"}, {"n": "b"}, {"n": "c"}]
})
app = mcp.http_app(path="/mcp", stateless_http=True)
1. Call the tool via HTTP POST to /mcp
2. Response hangs indefinitely for tools returning 3+ items in arrays
3. Tools returning 1-2 items work correctly
Python & MCP Python SDK
- MCP SDK version: 1.23.3
- Python version: 3.11
- FastMCP version: 2.13.1
- Transport: StreamableHTTP with stateless_http=True
Initial Checks
Description
Bug Description
SSE connections hang indefinitely when using StreamableHTTPServerTransport in stateless mode with responses containing 3+ items. The issue is caused by zero-buffer memory streams that block send() until receive() is called, creating a race condition between the response writer and the SSE stream iterator.
Related issues: #262 describes similar symptoms (client hangs on call_tool()) but root cause wasn't identified. This issue provides the specific cause and fix.
Expected Behavior
All tool responses should complete regardless of response size.
Actual Behavior
Root Cause Analysis
The issue is in mcp/server/streamable_http.py:
Line 412 - zero-buffer request stream
self._request_streams[request_id] = anyio.create_memory_object_streamEventMessage
Line 460 - zero-buffer SSE stream
sse_stream_writer, sse_stream_reader = anyio.create_memory_object_streamdict[str, str]
Race condition flow:
With zero-buffer streams, send() blocks until the receiver calls receive(). When the MCP server processes faster than the SSE writer task starts, deadlock occurs.
Why timing matters:
Proposed Fix
Increase buffer size from 0 to a reasonable value (e.g., 10 or 100):
Line 412
self._request_streams[request_id] = anyio.create_memory_object_streamEventMessage
Line 460
sse_stream_writer, sse_stream_reader = anyio.create_memory_object_streamdict[str, str]
Alternative fix: Use await tg.start() instead of tg.start_soon() to ensure SSE writer is ready before sending requests (requires EventSourceResponse to support task status protocol).
Workaround
We've applied this fix via sed patch in our Dockerfile:
RUN sed -i 's/create_memory_object_stream[EventMessage](0)/create_memory_object_streamEventMessage/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_streamdict[str, str]/g'
/usr/local/lib/python3.11/site-packages/mcp/server/streamable_http.py
This resolves the issue in our production environment.
Example Code
Python & MCP Python SDK