Repository navigation
session_idle_timeout is not exposed via streamable_http_app() and can cancel active requests #2455
Description
Activity
two bugs in the
session_idle_timeoutimplementation, both reproduced on currentmainandv1.x.problem 1:
streamable_http_app()silently dropssession_idle_timeout— it's accepted byStreamableHTTPSessionManager.__init__but not forwarded by eitherServer.streamable_http_app()(lowlevel) orMCPServer.streamable_http_app(), causing an immediateTypeError.problem 2: the idle
anyio.CancelScopewraps the entireapp.run()call. incoming requests push the deadline forward, but there's no guard that suspends the scope while a request is in-flight. if a handler takes longer thansession_idle_timeoutfrom the last push, the scope fires and the handler is cancelled mid-execution.workaround for problem 1: construct
StreamableHTTPSessionManagerdirectly instead of usingstreamable_http_app().
workaround for problem 2: none short of setting a very generous timeout.repro scripts + output
repro.py (problem 1):
from mcp.server import Server app = Server("demo") app.streamable_http_app(session_idle_timeout=30)
output:
TypeError: Server.streamable_http_app() got an unexpected keyword argument 'session_idle_timeout'repro_p2.py (problem 2 — replicates the CancelScope pattern from
streamable_http_manager.py:232-243):import anyio IDLE_TIMEOUT = 0.3 # 300ms — shorter than the simulated handler's 2s sleep async def main(): idle_scope = anyio.CancelScope() # simulate: request arrives, deadline pushed forward (manager.py:200-201) idle_scope.deadline = anyio.current_time() + IDLE_TIMEOUT with idle_scope: print(" [scope] handler started") await anyio.sleep(2.0) # simulates slow tool handler print(" [scope] handler completed") # never reached if idle_scope.cancelled_caught: print("REPRODUCED: handler cancelled mid-execution") anyio.run(main)
[scope] handler started REPRODUCED: CancelScope fired while handler was sleeping — handler never completedcode path (problem 2):
streamable_http_manager.py:232-235: createsCancelScopewithdeadline = now + session_idle_timeoutstreamable_http_manager.py:200-201: on each request, pushes deadline tonow + session_idle_timeoutstreamable_http_manager.py:237-243:with idle_scope: await self.app.run(...)— the scope is never suspended during request handling- when deadline expires the scope cancels
app.run()regardless of active handlers
suggested fix
// src/mcp/server/lowlevel/server.py def streamable_http_app( self, *, streamable_http_path: str = "/mcp", json_response: bool = False, stateless_http: bool = False, event_store: EventStore | None = None, retry_interval: int | None = None, + session_idle_timeout: float | None = None, transport_security: TransportSecuritySettings | None = None, ... ): session_manager = StreamableHTTPSessionManager( ... + session_idle_timeout=session_idle_timeout, ) // src/mcp/server/mcpserver/server.py (identical signature + forward change) // src/mcp/server/streamable_http_manager.py +import math ... -# Push back idle deadline on activity -if transport.idle_scope is not None and self.session_idle_timeout is not None: - transport.idle_scope.deadline = anyio.current_time() + self.session_idle_timeout # pragma: no cover -await transport.handle_request(scope, receive, send) +# Suspend idle deadline while the request is in-flight so a slow +# handler cannot be cancelled mid-execution, then reset after. +if transport.idle_scope is not None and self.session_idle_timeout is not None: + transport.idle_scope.deadline = math.inf +try: + await transport.handle_request(scope, receive, send) +finally: + if transport.idle_scope is not None and self.session_idle_timeout is not None: + transport.idle_scope.deadline = anyio.current_time() + self.session_idle_timeoutsame suspend/reset pattern applied to the new-session path (
http_transport.handle_requestcall aftertask_group.start).test to verify: creates a server with
session_idle_timeout=0.05sand alist_toolshandler that sleeps 0.15s, then asserts the handler completes and the client call succeeds.- addedbugSomething isn't workingSomething isn't workingready for workEnough information for someone to start working onEnough information for someone to start working onP2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable featurefix proposedBot has a verified fix diff in the commentBot has a verified fix diff in the comment
on Apr 17, 2026 thanks for the independent confirmation of both bugs. the suggested fix matches PR #2457 (suspend-then-reset the idle deadline around
handle_request+ forwardsession_idle_timeoutfrom bothServer.streamable_http_app()andMCPServer.streamable_http_app()). PR is up with a regression test that uses a 50ms idle timeout against a 150ms handler — feedback welcome.Picking this up. Approach matches the validation outline in the issue:
- Plumb
session_idle_timeoutthrough bothstreamable_http_app()wrappers (low-levelServerandMCPServer). - Suspend the idle deadline while at least one request is in flight. Using a per-transport
idle_active_requestscounter; on first increment setidle_scope.deadline = math.inf, on drop to 0 reset tonow + session_idle_timeout. This lets long-running tool calls finish even when they exceed the timeout. - Tests in
tests/server/test_streamable_http_manager.pycovering: (a) passthrough fromstreamable_http_app(session_idle_timeout=...)on both wrappers, (b) handler running longer than the timeout still completes, (c) existing idle reaping behavior intact once requests are finished, (d) deadline only resumes after the last concurrent request completes.
PR incoming.
- Plumb
Production data point in support of this issue, from a FastMCP streamable-HTTP server
(mcp1.27.1, uvicorn, loopback only, many short-lived MCP clients).Over a single 18.2 hour process lifetime the server logged:
Created new transport with session ID: ... 224 Session ... idle timeout 0 Cleaning up crashed session ... 0 DELETE /mcp 0 POST /mcp HTTP/1.1" 200 461 POST /mcp HTTP/1.1" 202 115 POST /mcp HTTP/1.1" 400 109224 sessions created, zero ever removed. Only 57 client sockets were still established at
the end, so roughly 167StreamableHTTPServerTransportobjects and theirrun_server
tasks were retained with no client behind them, for the lifetime of the process. Growth
was steady at about 12 leaked sessions per hour and is bounded only by process restarts.Cause, on 1.27.1:
mcp/server/streamable_http_manager.py:268-269only arms the idle cancel scope when
self.session_idle_timeout is not None.mcp/server/fastmcp/server.py:956-962builds the manager as
StreamableHTTPSessionManager(app=..., event_store=..., retry_interval=..., json_response=..., stateless=..., security_settings=...)with nosession_idle_timeout,
andFastMCP.settingshas no field for it, so for every FastMCP streamable-HTTP server
the value is unconditionallyNone.- The class docstring at
streamable_http_manager.py:58-62recommends 1800 for most
deployments, which no FastMCP user can currently set.
A client that goes away without sending
DELETE /mcp(process killed, laptop closed,
container reaped) therefore leaks its transport, its task and its SSE stream permanently.Suggested minimal fix for the FastMCP side: add
session_idle_timeout: float | None = None
toSettingsand passsession_idle_timeout=self.settings.session_idle_timeoutin the
StreamableHTTPSessionManager(...)call atfastmcp/server.py:956. The same argument
applies tomcp/server/lowlevel/server.py:746in 2.0.0, wherestreamable_http_app()
takesjson_response,stateless_http,event_store,retry_interval,
max_request_body_size,transport_securityandhostbut still not
session_idle_timeout, so 2.0.0 inherits the same defect.Workaround currently in use, for anyone hitting this on 1.x, using only public surface:
mcp.streamable_http_app() # create the manager eagerly mcp.session_manager.session_idle_timeout = 1800.0 # public attribute mcp.run(transport="streamable-http") # reuses the same manager
Verified: with the timeout set to 3 seconds, an initialized session logs
Session <id> idle timeoutthree seconds later and is removed from_server_instances.
Without it, nothing is ever logged.- added a commit that references this issue
on Aug 21, 2026
Summary
The
session_idle_timeoutfeature currently has two linked problems:StreamableHTTPSessionManagerbut not exposed through the canonicalstreamable_http_app()API surfaceWhy this looks like a bug
PR #1994 / #2022 describe
session_idle_timeoutas a way to reap sessions that receive no HTTP requests for the configured duration.That implies two things:
Today neither of those expectations holds.
Problem 1: not exposed by
streamable_http_app()StreamableHTTPSessionManager(...)acceptssession_idle_timeout, but:mcp.server.lowlevel.server.Server.streamable_http_app(...)does notmcp.server.mcpserver.server.MCPServer.streamable_http_app(...)does notSo the recommended high-level API cannot configure a supported session-manager feature.
Reproduction
Observed behavior
Problem 2: active requests can be cancelled by the idle timeout
With a short idle timeout and a handler that takes longer than that timeout, the session can be reaped mid-request and the client waits until timeout / session termination rather than receiving the tool result.
Reproduction outline
StreamableHTTPSessionManager(app=..., session_idle_timeout=...)Observed behavior
On current
main, the active request can be terminated before the handler finishes, and the client fails instead of receiving the result.Expected behavior
streamable_http_app()should exposesession_idle_timeoutValidation
I reproduced both behaviors locally against current
main.I also verified that a minimal patch can fix both together by:
session_idle_timeoutthrough the low-level + MCPServerstreamable_http_app()wrappersA focused regression suite in
tests/server/test_streamable_http_manager.pycan cover:streamable_http_app(session_idle_timeout=...)