Skip to content

Streamable HTTP session lifecycle can retain unreachable sessions and serialize session creation behind slow requests #3605

Closed

Description

@fegloff

Summary

While implementing a real MCP readiness probe against the Python SDK's stateful Streamable HTTP transport, we found several related session-lifecycle behaviors that can leave server-side sessions retained even though no client can use or close them.

Observed with mcp==1.27.1.

The cases are especially visible for short-lived probes, but they apply to ordinary clients as well.

Observed behaviors

1. Closed sessions remain retained

After a client closes a session with DELETE, the session is closed but remains present in the session manager's internal session registry.

For a long-running server with repeated short-lived sessions, closed entries can therefore accumulate unless the embedding application adds its own cleanup.

2. A session can be registered before the initiating request is ultimately accepted

A new session may be registered server-side before later request validation rejects the request.

We observed this around cases such as an invalid Host request. Similar orphaning is possible when initialization is interrupted after the server has registered the session but before the client receives the session id.

The important lifecycle problem is:

  1. the server creates/registers a session;
  2. the request fails or is interrupted before the client learns the session id;
  3. the client therefore cannot address that session;
  4. the client cannot send DELETE;
  5. the server retains a session that no client can clean up.

This also matters when a readiness probe times out or is killed during initialization.

3. Session creation can be serialized behind a slow request

The session-creation path uses shared synchronization such that a slow/stalled opening request can hold the creation path long enough to delay unrelated clients attempting to create sessions.

A client that uploads or stalls slowly during this path can therefore affect other clients' ability to initialize.

Expected behavior

Ideally the SDK would provide lifecycle semantics such that:

  • a session successfully closed with DELETE is removed from the session registry when it is no longer needed;
  • if an opening request creates/registers a session but fails before the client can receive/use its session id, that session is automatically closed and removed;
  • interrupted initialization does not leave indefinitely retained unreachable sessions;
  • a slow opening request does not unnecessarily serialize unrelated session creation;
  • applications do not need to infer whether a session was ever successfully exposed to a client in order to garbage-collect it safely.

If intentional retention is required for protocol reasons, an SDK-owned bounded cleanup/reaping mechanism would avoid each embedding server having to implement its own session lifecycle policy.

Current application workaround

Our server currently distinguishes sessions that a client has demonstrated knowledge of from sessions whose id was never presented back by any request.

For an unclaimed session, a grace period starts only after the request that opened it finishes. If no request ever presents that session id, a later request can close/remove the abandoned session after the grace period.

Once any request presents the session id, the session is treated as claimed and is not subject to this orphan cleanup, even after a long idle period.

This avoids expiring legitimate long-lived MCP sessions while bounding sessions that became unreachable during initialization.

We also added a regression test specifically detecting the SDK's retention of closed sessions so the workaround can be removed when upstream lifecycle behavior changes.

This is only orphan cleanup; it is not an authentication or resource-quota mechanism. A caller that can reach an unauthenticated MCP endpoint can intentionally establish and keep valid sessions open.

Why this matters for readiness probes

A useful readiness check should exercise the real MCP surface:

initialize -> initialized -> tools/list -> DELETE

Normally that cleans up correctly. But the probe process may be timed out/killed, or initialization may be rejected after the server has already registered state. In those cases the client may never receive the session id and therefore cannot perform cleanup itself.

That turns routine readiness probing into a source of retained server-side state unless the application compensates for the SDK lifecycle behavior.

Environment

  • Python MCP SDK: mcp==1.27.1
  • Stateful Streamable HTTP session manager
  • Reproduced with local deterministic tests around abandoned initialization, rejected requests, delayed opening requests, explicit DELETE, and long-lived claimed sessions.

I can provide a minimal reproduction or point to the application-side tests/workaround if useful.

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Oct 1, 2026
  2. Ben-Home commented on Oct 1, 2026

    @Ben-Home

    This report is unusually well characterized, and the four-behaviors framing is the right one, because they share a single root cause: session registration and session lifetime are not tied to the response that would let a client close the session.

    Taking the orphan case you describe, the sequence is worth restating as an invariant violation rather than a leak:

    1. the server registers a session and allocates an id
    2. validation fails, or the response is interrupted, before the client receives that id
    3. the client therefore has no handle to address the session with
    4. DELETE cannot be sent, because there is nothing to send it against
    5. the server retains a session no client can ever close

    Step 5 follows necessarily from step 2, which means registration cannot happen before the response is committed to. The clean fix is to allocate the session id optimistically but only publish it into the registry once the initiating response is successfully handed off. A session that is created and then abandoned before the id ships should be rolled back, not retained. That covers the invalid Host case and the interrupted-initialization case with the same mechanism.

    For the DELETE case, closed entries remaining in the registry reads as a cleanup ordering issue: the session is marked closed but removal from the registry is separate and apparently conditional. Making close and deregister a single operation removes the accumulation without requiring the embedding application to add its own reaper.

    Your point about short-lived probes is the reason this matters more than it might appear. Stateful Streamable HTTP with a readiness probe generates a fresh session per probe, so any per-session retention that is not bounded by the client's ability to close it grows monotonically under normal health-check traffic. That is the worst shape for this class, because the load pattern that triggers it is the one you add specifically to confirm things are healthy.

    The pattern I would hold this to: for every registered session there should be exactly one reachable path to deregistration, and it should not depend on the client having received something. If a session can be created by a request whose response never lands, retention has to be self-limiting rather than client-driven. Where the client is genuinely responsible, an idle timeout keyed on the last request is the usual backstop, and it also covers the session-serialization symptom in your fourth case, since a slow request holding the registry is the same as a session that cannot be closed.

    This is the invariant we enforce on the connector layer at corpusiq.io for the same reason: sessions that outlive their client are indistinguishable from live load, so the only safe assumption is that the client will never come back to close them.

  3. Jaswanth1902 commented on Oct 2, 2026

    @Jaswanth1902
  4. maxisbey commented on Oct 6, 2026

    @maxisbey
    Contributor

    Thanks for the careful write-up. All three of these have been fixed since 1.27.1, so I'm going to close this.

    The changes are in #3395 (released in 2.2.0) and its backport #3426 (1.30.0):

    • A client DELETE now removes the session from the manager straight away.
    • If the opening request is refused, fails or is cancelled, the session it created is discarded rather than kept.
    • The creation lock now only covers admitting the session. The opening request is served outside it, so one slow client no longer holds up the others.

    That leaves the case where initialize succeeds but the client dies before using the session id. Idle expiry covers that: a stateful session with nothing in flight for 30 minutes is closed by default (session_idle_timeout=), and there's a cap on concurrent sessions (max_sessions=).

    So upgrading to 1.30.0 or 2.x should let you drop the orphan cleanup. If your regression test still catches something after that, please open a new issue with it.

    AI Disclaimer


    Generated by Claude Code

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

    v1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions