Title: [BUG] McpStreamableServerSession does not close server-side socket when client disconnects, causing CLOSE-WAIT leak and thread pool exhaustion · Issue #1021 · modelcontextprotocol/java-sdk · GitHub
Open Graph Title: [BUG] McpStreamableServerSession does not close server-side socket when client disconnects, causing CLOSE-WAIT leak and thread pool exhaustion · Issue #1021 · modelcontextprotocol/java-sdk
X Title: [BUG] McpStreamableServerSession does not close server-side socket when client disconnects, causing CLOSE-WAIT leak and thread pool exhaustion · Issue #1021 · modelcontextprotocol/java-sdk
Description: Description When using the MCP Java SDK's Streamable HTTP server transport (via spring-ai-starter-mcp-server-webmvc), the server-side socket is not properly closed after the client disconnects (sends TCP FIN). This causes connections to ...
Open Graph Description: Description When using the MCP Java SDK's Streamable HTTP server transport (via spring-ai-starter-mcp-server-webmvc), the server-side socket is not properly closed after the client disconnects (sen...
X Description: Description When using the MCP Java SDK's Streamable HTTP server transport (via spring-ai-starter-mcp-server-webmvc), the server-side socket is not properly closed after the client disconnects ...
Opengraph URL: https://github.com/modelcontextprotocol/java-sdk/issues/1021
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"[BUG] McpStreamableServerSession does not close server-side socket when client disconnects, causing CLOSE-WAIT leak and thread pool exhaustion","articleBody":"## Description\n\nWhen using the MCP Java SDK's Streamable HTTP server transport (via `spring-ai-starter-mcp-server-webmvc`), the server-side socket is not properly closed after the client disconnects (sends TCP FIN). This causes connections to remain in `CLOSE-WAIT` state indefinitely, each holding a Tomcat worker thread. Under moderate load, the entire Tomcat thread pool is exhausted within seconds, making the server completely unresponsive to any new requests including health checks.\n\n## Environment\n\n- Spring AI: 1.1.4\n- MCP Java SDK: (bundled with Spring AI 1.1.4)\n- Java: JDK 25\n- Server: Tomcat 10.1.34 (WAR deployment via Spring Boot 3.4.1)\n- Transport: Streamable HTTP (`spring.ai.mcp.server.protocol=STREAMABLE`)\n- OS: Linux (Kubernetes pod, 2 CPU / 4GB RAM)\n\n## Configuration\n\n```yaml\nspring:\n ai:\n mcp:\n server:\n type: SYNC\n protocol: STREAMABLE\n streamable-http:\n mcp-endpoint: /mcp\n keep-alive-interval: 0s\n```\n\n## Steps to Reproduce\n\n1. Deploy an MCP Server with Streamable HTTP transport (WebMVC, SYNC mode)\n2. Have an external MCP client send requests to `POST /mcp` (initialize + tools/call)\n3. Client receives the tool response and closes the TCP connection (sends FIN)\n4. Repeat with multiple clients (or a single client with retry logic)\n5. Observe server-side socket states with `ss -tnp | grep 8080`\n\n## Observed Behavior\n\nAfter the client closes the connection:\n\n- Server-side socket enters `CLOSE-WAIT` and is **never** closed\n- The Tomcat worker thread handling that request is never released back to the pool\n- Under load from a single upstream LB doing health-check retries, all 150 Tomcat threads are exhausted within ~30 seconds\n- New connections (including K8s readiness probes) queue in the TCP backlog and time out\n\n```\n$ ss -tlnp | grep 8080\nLISTEN 151 150 *:8080 *:*\n\n$ ss -tnp | grep 8080 | head -5\nCLOSE-WAIT 115 0 [::ffff:10.125.87.86]:8080 [::ffff:10.125.87.4]:47140\nCLOSE-WAIT 115 0 [::ffff:10.125.87.86]:8080 [::ffff:10.125.87.4]:42756\nCLOSE-WAIT 115 0 [::ffff:10.125.87.86]:8080 [::ffff:10.125.87.4]:47446\nCLOSE-WAIT 115 0 [::ffff:10.125.87.86]:8080 [::ffff:10.125.87.4]:50138\nCLOSE-WAIT 115 0 [::ffff:10.125.87.86]:8080 [::ffff:10.125.87.4]:43160\n\n$ ss -tnp | grep 8080 | wc -l\n150\n\n$ curl --max-time 5 http://localhost:8080/health\ncurl: (28) Failed to connect to localhost port 8080: Connection timed out\n```\n\nAll 150 connections are from the same upstream IP (load balancer), all in CLOSE-WAIT.\n\n## Expected Behavior\n\nWhen the client closes the TCP connection (sends FIN), the server should:\n\n1. Detect the peer shutdown (e.g., via IOException on write, or checking `SocketChannel.read() == -1`)\n2. Close the SSE stream / Reactor Sink associated with that session\n3. Remove the session from the internal session map\n4. Close the server-side socket\n5. Release the Tomcat thread back to the pool\n\n## Root Cause Analysis\n\nThe MCP Streamable HTTP transport opens an SSE stream for each session. When the client disconnects:\n- The server-side `Sinks.Many` has no subscribers, but the stream is never terminated\n- The Servlet async context is never completed\n- The socket remains open on the server side (only client sent FIN)\n- Tomcat's NIO connector holds the thread waiting for the async context to complete\n\n## Impact\n\n- **Severity: Critical** — renders the server completely unresponsive\n- Makes rolling deployments impossible in production (new pods get flooded by retrying clients immediately after startup)\n- K8s readiness probes fail → pod marked unhealthy → never enters service\n- No automatic recovery — requires pod restart AND stopping upstream traffic simultaneously\n\n## Workaround\n\nSet Tomcat connection timeout to force-close idle connections:\n\n```yaml\nserver:\n tomcat:\n connection-timeout: 30000\n keep-alive-timeout: 30000\n max-connections: 200\n threads:\n max: 200\n```\n\nThis allows Tomcat to reclaim CLOSE-WAIT connections after 30 seconds, but is not a proper fix — it just limits the damage window.\n\n## Suggested Fix\n\nThe Streamable HTTP transport provider should register a listener for client disconnect events. In the WebMVC integration:\n\n```java\n// When setting up the async response for SSE:\nasyncContext.addListener(new AsyncListener() {\n @Override\n public void onComplete(AsyncEvent event) {\n cleanupSession(sessionId);\n }\n @Override\n public void onTimeout(AsyncEvent event) {\n cleanupSession(sessionId);\n }\n @Override\n public void onError(AsyncEvent event) {\n cleanupSession(sessionId);\n }\n // ...\n});\n```\n\nOr detect write failures when attempting to send data to the client and trigger session cleanup.\n","author":{"url":"https://github.com/lxq19991111","@type":"Person","name":"lxq19991111"},"datePublished":"2026-06-11T03:41:02.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":3},"url":"https://github.com/1021/java-sdk/issues/1021"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:e439add6-5881-5da2-86ec-e06bea17f673 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | BC98:2681A6:BA1F5A:F3479E:6A5B025F |
| html-safe-nonce | e889da18807a717697c8252cc62d692c16a14d96c228f11aac784df9284a2b62 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCQzk4OjI2ODFBNjpCQTFGNUE6RjM0NzlFOjZBNUIwMjVGIiwidmlzaXRvcl9pZCI6IjIzMzM3MDUzNTcyMDAzOTI3OTkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 3ca81e267718123234468b102e47f8ff42842819938b5316edd93b92fe8b5857 |
| hovercard-subject-tag | issue:4637010934 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/modelcontextprotocol/java-sdk/1021/issue_layout |
| twitter:image | https://opengraph.githubassets.com/5e62f8b22ca5218e94d357989604eb28a18e96b01d3dad2ef7e4d9d610dce552/modelcontextprotocol/java-sdk/issues/1021 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/5e62f8b22ca5218e94d357989604eb28a18e96b01d3dad2ef7e4d9d610dce552/modelcontextprotocol/java-sdk/issues/1021 |
| og:image:alt | Description When using the MCP Java SDK's Streamable HTTP server transport (via spring-ai-starter-mcp-server-webmvc), the server-side socket is not properly closed after the client disconnects (sen... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | lxq19991111 |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| go-import | github.com/modelcontextprotocol/java-sdk git https://github.com/modelcontextprotocol/java-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 919609219 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/java-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 919609219 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/java-sdk |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width