Skip to content

Race condition in StreamableHTTP: zero-buffer memory streams cause deadlock with concurrent SSE responses #1764

@Ctariy

Description

@Ctariy

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:

  1. tg.start_soon(response, ...) starts SSE response task (non-blocking)
  2. await writer.send(session_message) sends request to MCP server
  3. MCP server processes quickly and calls message_router
  4. message_router tries await request_streams[id][0].send(EventMessage(...))
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Significant bug affecting many users, highly requested featurebugSomething isn't workingfix proposedBot has a verified fix diff in the commentready for workEnough information for someone to start working on

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions