Repository navigation
[BUG] KeepAliveScheduler does not evict sessions after ping failure, causing unbounded CPU growth #1022
Description
Description
When keep-alive-interval is configured on a Streamable HTTP MCP Server, KeepAliveScheduler periodically sends ping messages to all registered McpStreamableServerSession instances. When a session's stream is unavailable (client already disconnected), the ping fails and a WARN is logged, but the session is never removed from the session map. Over time, dead sessions accumulate indefinitely, causing increasing CPU usage as the scheduler iterates over a growing list of unreachable sessions every interval.
Environment
- Spring AI: 1.1.4
- MCP Java SDK: (bundled with Spring AI 1.1.4)
- Java: JDK 25
- Server: Tomcat (embedded via Spring Boot 3.4.1)
- Transport: Streamable HTTP (
spring.ai.mcp.server.protocol=STREAMABLE)
Configuration
spring:
ai:
mcp:
server:
type: SYNC
protocol: STREAMABLE
streamable-http:
mcp-endpoint: /mcp
keep-alive-interval: 30sSteps to Reproduce
- Deploy an MCP Server with Streamable HTTP transport and
keep-alive-interval: 30s - Have multiple MCP clients connect, call tools, then disconnect (close TCP)
- Wait a few minutes and observe logs and CPU usage
Observed Behavior
Logs fill with repeated warnings every 30 seconds, one per dead session:
WARN [io.modelcontextprotocol.util.KeepAliveScheduler]
Failed to send keep-alive ping to session McpStreamableServerSession@3656e950: Stream unavailable for session d0a2a5b9-d427-49f6-8f0b-6c37293e2e35
WARN [io.modelcontextprotocol.util.KeepAliveScheduler]
Failed to send keep-alive ping to session McpStreamableServerSession@917a226: Stream unavailable for session 5e87f163-f127-4793-9fec-cd35b6a59327
WARN [io.modelcontextprotocol.util.KeepAliveScheduler]
Failed to send keep-alive ping to session McpStreamableServerSession@5795b3a2: Stream unavailable for session ...
- Dead sessions are never removed from the internal map
- CPU usage grows linearly with the number of accumulated dead sessions
- The
boundedElasticthread pool is consumed by failed ping attempts - Eventually causes CPU alerts and degrades ability to serve new requests
In our production environment, CPU went from normal to sustained 135%+ within hours of deployment, triggered by ~50 disconnected clients accumulating dead sessions.
Expected Behavior
When a ping fails (stream unavailable), KeepAliveScheduler should:
- Increment a failure counter for that session
- After N consecutive failures (e.g., 2-3), call
session.close()and remove it from the session map - Log the eviction at INFO level
Impact
- Severity: High — causes progressive CPU degradation in production
- Combined with the CLOSE-WAIT socket leak issue, can render a server completely unusable
- No recovery without restart — dead sessions accumulate forever
Workaround
Disable keep-alive entirely:
spring:
ai:
mcp:
server:
streamable-http:
keep-alive-interval: 0sThis stops the CPU waste but also removes the ability to detect stale sessions proactively.
Suggested Fix
In KeepAliveScheduler, after a failed ping:
// Pseudocode
void sendKeepAlive() {
Iterator<Map.Entry<String, McpStreamableServerSession>> it = sessions.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, McpStreamableServerSession> entry = it.next();
try {
entry.getValue().sendPing();
failureCounters.remove(entry.getKey());
} catch (Exception e) {
int failures = failureCounters.merge(entry.getKey(), 1, Integer::sum);
if (failures >= MAX_FAILURES) {
entry.getValue().close();
it.remove();
failureCounters.remove(entry.getKey());
log.info("Evicted dead session: {}", entry.getKey());
}
}
}
}
Activity
nikita-kibitkin commentedon Jun 12, 2026
I can take this.
Looking at the code, KeepAliveScheduler only receives a Supplier<Flux> from the transport provider, so it cannot evict from the session map directly. I would add an on-failure hook to the scheduler and wire it in HttpServletStreamableServerTransportProvider, plus any other provider that creates a KeepAliveScheduler, so a failed keep-alive can close and remove the session through the existing transport cleanup path. Successful pings would stay unchanged, and this would not introduce a general session TTL policy.
I would not evict on the first failure: #920 shows transient write failures can happen, and a streamable client without an open GET stream can still be alive and POSTing. A small consecutive-failure threshold, like the 2-3 failures suggested in the issue, seems like the right boundary.
If that scope works, I will put up a PR.
lxq19991111 commentedon Jun 12, 2026
Thanks for digging into this. @nikita-kibitkin
I’ve already opened the related upstream MCP Java SDK issue here: #1021
Since the relevant pieces (
KeepAliveScheduler, streamable session handling, and transport cleanup) live in the MCP Java SDK, I’ve started working on the upstream fix there first and will link the PR back here once it is ready.My expectation is that this Spring AI issue will likely be resolved primarily through the MCP-side change, with Spring AI then validating the upgraded behavior and only adding follow-up integration changes if needed.
If you’d still like to collaborate, that would be very helpful — especially on validating the behavior from the Spring AI side, regression coverage, and checking whether any Spring-specific follow-up is still needed after the MCP fix lands.
lxq19991111 commentedon Jun 13, 2026
Small correction from my side: I realized I mixed up the issue context in my previous reply. I thought I was replying to the related Spring AI issue here: spring-projects/spring-ai#6384, but this issue is already in the MCP Java SDK repo.
To clarify:
KeepAliveSchedulerdead-session eviction problem.I’ve already spent time digging into the streamable session lifecycle while preparing #1027, and I’d be interested in helping with this follow-up as well.
Your proposed direction makes sense to me: add a keep-alive failure hook / consecutive failure threshold, and let the transport provider perform session cleanup because it owns the session registry. I also agree eviction should not happen on the first failure.
If you have not started on the implementation yet, I’d be happy to prepare a small draft PR based on the direction you outlined, with focused tests. I’d also very much welcome your review and input on the approach. @nikita-kibitkin
lxq19991111 commentedon Jun 13, 2026
Thanks for confirming this, and also thanks again for the guidance on #1021.
For this issue, your production observations line up with the failure mode described here: keep-alive currently detects dead sessions by logging failed pings, but it does not help the transport recover, so the scheduler keeps doing repeated failed work every interval.
I agree with the direction of evicting sessions after failed pings, but I have two concerns about the exact boundary:
KeepAliveSchedulercurrently receives sessions from a supplier and does not own the session registry, so I think it should report failures rather than directly remove from the map.Based on the discussion so far, I’m thinking of preparing a conservative MCP SDK draft PR with this shape:
KeepAliveScheduler.This should address the resource leak while keeping the first change focused and avoiding a general session lifetime policy.
I’ll put together a small draft PR along these lines and would very much welcome your review and input, especially on the failure threshold and whether TTL should be included now or deferred. @nikita-kibitkin @Ben-Home
lxq19991111 commentedon Jun 14, 2026
I opened #1028 for the Streamable HTTP keep-alive dead-session eviction part of this issue.
The PR adds keep-alive success/failure hooks to
KeepAliveScheduler, tracks consecutive failures inHttpServletStreamableServerTransportProvider, and evicts/closes the session after 3 consecutive failed keep-alive pings.I intentionally kept the eviction logic in the Streamable HTTP servlet provider rather than in
KeepAliveScheduler, because the provider owns the session registry and can safely remove only the currently registered session instance.I also left the deprecated SSE servlet provider unchanged.
Testing:
mvn -pl mcp-core -Dtest=KeepAliveSchedulerTests,HttpServletStreamableServerTransportProviderTests testmvn -pl mcp-core testKehrlann commentedon Sep 1, 2026
This has been reported originally in #471 .
Let's keep the discussion here, as it has more context.