Skip to content

[BUG] WebMvcStreamableServerTransportProvider: session and socket leak when clients disconnect without DELETE #6860

Open

Description

@lxq19991111

[BUG] WebMvcStreamableServerTransportProvider: session and socket leak when clients disconnect without DELETE

Description

WebMvcStreamableServerTransportProvider leaks MCP sessions and their associated TCP sockets permanently when a client disconnects without sending an explicit DELETE /mcp request. Over time this exhausts server file descriptors and makes the application unresponsive.

The root cause is a combination of:

  1. sessions.remove(sessionId) is only called inside handleDelete — no other code path evicts a session.
  2. SseBuilder.onTimeout() only logs; it does not close the stream or remove the session.
  3. SSE write failures (SseBuilder.send() throwing) do not trigger transport cleanup.
  4. KeepAliveScheduler swallows ping failures via .onErrorComplete() and never removes dead sessions.
  5. There is no session TTL or background reaper.

Any client that goes away without a clean DELETE (network drop, process kill, laptop sleep, load-balancer timeout) leaks a session and socket permanently.

Environment

  • Spring AI: 1.1.4 – 1.1.8 (all use WebMvcStreamableServerTransportProvider)
  • Spring Boot: 3.4.x / 3.5.x
  • MCP Java SDK: 0.17.0 – 0.18.3 (bundled via Spring AI)
  • Transport: Streamable HTTP with spring-ai-starter-mcp-server-webmvc
  • Server: Tomcat (both embedded and standalone WAR)
  • OS: Linux (Kubernetes)

Production Evidence

Reproduced independently by two users in production:

User 1 (reported in spring-projects/spring-ai#6384)

  • 138 CLOSE-WAIT sockets accumulated within minutes under moderate load
  • Tomcat listen backlog full (151/150), health checks timing out
  • connectionTimeout ineffective because connections are held by startAsync()
  • Only recovered after ~2 hours via kernel tcp_keepalive_time

User 2 (reported in modelcontextprotocol/java-sdk#1021)

  • Measured over 48 hours on a single host:
elapsed tomcat.connections.current open FDs
baseline 2 72
+~2h 20 91
+~48h 6373 6443
  • FD count only ratchets up, never decreases during idle periods
  • Earlier instance reached 8290 FDs after ~4.5 days
  • Log signature: Failed to send keep-alive ping to session ...: Stream unavailable for session <id>
  • On shutdown: Failed to complete SSE builder for session {}: The response object has been recycled

Steps to Reproduce

  1. Deploy an MCP server using spring-ai-starter-mcp-server-webmvc with Streamable HTTP transport.
  2. Connect an MCP client, complete initialization.
  3. Terminate the client without sending DELETE (e.g. kill -9, network drop).
  4. Repeat steps 2–3.
  5. Observe: session count and socket count only increase; keep-alive warnings appear once per interval per dead session.

Expected Behavior

When a client disconnects (SSE write failure, timeout, or async error):

  1. The current SSE stream/transport should be closed.
  2. The servlet async context should be completed, releasing the Tomcat NIO channel.
  3. After repeated keep-alive failures or a configurable TTL, the logical MCP session should be evicted from sessions.
  4. The server-side socket should be closed, preventing CLOSE-WAIT accumulation.

Root Cause Analysis

In WebMvcStreamableServerTransportProvider:

// onTimeout — only logs, does not close
sseBuilder.onTimeout(() -> log.debug("SSE connection timed out for session: {}", sessionId));

// onComplete — closes listeningStream, but only this one path
sseBuilder.onComplete(() -> listeningStream.close());

// sendMessage write failure — calls sseBuilder.error(), does not close transport
catch (Exception e) {
    sseBuilder.error(e);  // does not trigger session/transport cleanup
}

In KeepAliveScheduler:

// ping failure is swallowed, session never removed
.onErrorComplete()

There is no sessions.remove() outside of handleDelete, no TTL, and no reaper.

Relationship to MCP Java SDK java-sdk#1027

I previously filed spring-projects/spring-ai#6384 for this issue. A Spring AI maintainer (@sdeleuze) closed it, directing the fix to the MCP Java SDK side. I then submitted modelcontextprotocol/java-sdk#1027 to address the servlet Streamable HTTP transport lifecycle.

However, as clarified in the MCP Java SDK 1.0.0 release notes (February 2026):

"Moved mcp-spring-webflux and mcp-spring-webmvc to Spring AI 2.0" (#805)

The Spring MVC transport has been maintained by Spring AI since February 2026. PR modelcontextprotocol/java-sdk#1027 only fixes HttpServletStreamableServerTransportProvider in mcp-core — a different code path that is not used by spring-ai-starter-mcp-server-webmvc. Therefore, modelcontextprotocol/java-sdk#1027 does not address this issue for any Spring AI WebMVC user.

Proposed Fix

I would like to contribute a PR to address this. The fix mirrors the approach in MCP SDK #1027:

  1. onTimeout → close the listening stream / transport, not just log.
  2. onError → close the transport, completing the async context.
  3. SSE write failure → call transport.close(), not just sseBuilder.error().
  4. Keep-alive ping failure → evict the dead session from sessions after N consecutive failures (or integrate with session TTL logic similar to #1028).
  5. Add regression tests covering client disconnect, timeout, and write-failure scenarios.

The fix should be minimal and non-breaking — closing only the affected physical stream/transport, while preserving the logical session for legitimate reconnects within a bounded window.

If maintainers agree with this direction, I can submit a PR against main targeting the mcp/transport/mcp-spring-webmvc module.

Related

Activity

  1. added 2 commits that reference this issue on Aug 28, 2026
    b10647a
    e4325f5
  2. added a commit that references this issue on Sep 10, 2026
    9601a93
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions