Repository navigation
[BUG] WebMvcStreamableServerTransportProvider: session and socket leak when clients disconnect without DELETE #6860
Description
[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:
sessions.remove(sessionId)is only called insidehandleDelete— no other code path evicts a session.SseBuilder.onTimeout()only logs; it does not close the stream or remove the session.- SSE write failures (
SseBuilder.send()throwing) do not trigger transport cleanup. KeepAliveSchedulerswallows ping failures via.onErrorComplete()and never removes dead sessions.- 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
connectionTimeoutineffective because connections are held bystartAsync()- 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
- Deploy an MCP server using
spring-ai-starter-mcp-server-webmvcwith Streamable HTTP transport. - Connect an MCP client, complete initialization.
- Terminate the client without sending
DELETE(e.g.kill -9, network drop). - Repeat steps 2–3.
- 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):
- The current SSE stream/transport should be closed.
- The servlet async context should be completed, releasing the Tomcat NIO channel.
- After repeated keep-alive failures or a configurable TTL, the logical MCP session should be evicted from
sessions. - 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-webfluxandmcp-spring-webmvcto 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:
onTimeout→ close the listening stream / transport, not just log.onError→ close the transport, completing the async context.- SSE write failure → call
transport.close(), not justsseBuilder.error(). - Keep-alive ping failure → evict the dead session from
sessionsafter N consecutive failures (or integrate with session TTL logic similar to #1028). - 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
- spring-projects/spring-ai#6384 — my original report (closed, was redirected to MCP SDK)
- modelcontextprotocol/java-sdk#1021 — upstream issue with production data from two independent users
- modelcontextprotocol/java-sdk#1027 — my PR fixing the servlet transport (does not cover WebMVC)
- modelcontextprotocol/java-sdk#1028 — session TTL / keep-alive eviction (servlet only)
- modelcontextprotocol/java-sdk#805 — migration of Spring transports to Spring AI (February 2026)
Activity
Close WebMVC Streamable transports on disconnect
Close WebMVC Streamable transports on disconnect
Close WebMVC Streamable transports on disconnect