spring-projects / spring-projects/spring-ai
[BUG] WebMvcStreamableServerTransportProvider: session and socket leak when clients disconnect without DELETE
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.5k
- Forks
- 2.9k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 6
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 #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 #1027 only fixes HttpServletStreamableServerTransportProvider in mcp-core — a different code path that is not used by spring-ai-starter-mcp-server-webmvc. Therefore, #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)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in the mcp/transport/mcp-spring-webmvc module with WebMvcStreamableServerTransportProvider and its KeepAliveScheduler. Trace the onTimeout, onComplete, SSE send-failure, and keep-alive failure paths, then review the proposed regression scenarios for disconnect, timeout, and write failure. Done means affected streams and async contexts are released and dead sessions are evicted without breaking legitimate reconnects.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring
- Domain
- api, backend, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100