HttpClient resource leak causes thread accumulation and memory exhaustion
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 38/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- java
- Domain
- networking
Research direction
Start by reading HttpClientStreamableHttpTransport#build() and the corresponding HttpClientSseClientTransport builder, then trace how client.closeGracefully() reaches transport shutdown. Compare the lifecycle of each created HttpClient with the observed SelectorManager threads. Done means repeated transport creation and closure no longer accumulates those threads or exhausts memory.
Written by the indexing model from the issue text.
Description
Bug description
When using HttpClientStreamableHttpTransport and HttpClientSseClientTransport, the application experiences continuous accumulation of HttpClient-xxxx-SelectorManager threads that are never cleaned up, eventually leading to memory exhaustion and application instability.
The root cause is that each transport builder creates a new HttpClient instance via HttpClient.Builder.build(), but these HttpClient instances are never properly closed when the transport shuts down. Each HttpClient spawns dedicated SelectorManager threads for network I/O operations, and since OpenJDK's HttpClient lacks public APIs for resource cleanup, these threads remain active indefinitely.
Technical Details: Tracing through HttpClientStreamableHttpTransport#build() reveals that each HttpClient instantiation triggers the creation of a SelectorManager thread in the OpenJDK 17 source code:
SelectorManager(HttpClientImpl ref) throws IOException {
super(null, null,
"HttpClient-" + ref.id + "-SelectorManager",
0, false);
owner = ref;
debug = ref.debug;
debugtimeout = ref.debugtimeout;
pool = ref.connectionPool();
registrations = new ArrayList<>();
deregistrations = new ArrayList<>();
selector = Selector.open();
}
Source: OpenJDK 17 HttpClientImpl.java
This constructor shows how each HttpClient creates a uniquely named SelectorManager thread ("HttpClient-" + ref.id + "-SelectorManager"), which explains the observed thread naming pattern in production environments.
Environment
- Spring MCP Version: Latest (current main branch)
- Java Version: OpenJDK 17+ (tested on OpenJDK 17.0.14)
- Operating System: macOS 14.6.0 (also reproducible on Linux)
- Transport Types:
HttpClientStreamableHttpTransport,HttpClientSseClientTransport - Related OpenJDK Issue: JDK-8308364
Steps to reproduce
- Create multiple
HttpClientStreamableHttpTransportinstances:
for (int i = 0; i < 10; i++) {
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport
.builder("http://localhost:8080")
.build();
McpSyncClient client = McpClient.sync(transport).build();
client.initialize();
client.closeGracefully(); // This doesn't clean up HttpClient threads
}
- Monitor system threads using
jstackor thread monitoring tools - Observe continuous growth of
HttpClient-xxxx-SelectorManagerthreads - Repeat the process multiple times to see thread accumulation
Expected behavior
- When
transport.closeGracefully()is called, all associated HttpClient resources should be cleaned up HttpClient-xxxx-SelectorManagerthreads should be terminated and not accumulate- Memory usage should remain stable across multiple transport creation/destruction cycles
- No thread leakage should occur in long-running applications
Minimal Complete Reproducible example
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
public class HttpClientLeakDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("Initial thread count: " + Thread.activeCount());
// Create and close multiple transports
for (int i = 0; i < 20; i++) {
System.out.println("\n=== Creating transport " + (i + 1) + " ===");
var transport = HttpClientSseClientTransport
.builder("http://127.0.0.1:8002") // your sse mcp server base url
.build();
McpSyncClient client = McpClient.sync(transport)
.requestTimeout(Duration.ofSeconds(5))
.build();
try {
// This will fail but still creates the HttpClient
client.initialize();
} catch (Exception e) {
System.out.println("Expected initialization failure: " + e.getMessage());
}
// Close the client - this should clean up resources but doesn't
client.closeGracefully();
System.out.println("Thread count after closing transport " + (i + 1) + ": " + Thread.activeCount());
// List HttpClient threads
Thread.getAllStackTraces().keySet().stream()
.filter(t -> t.getName().contains("HttpClient") && t.getName().contains("SelectorManager"))
.forEach(t -> System.out.println(" - " + t.getName()));
}
System.out.println("\nFinal thread count: " + Thread.activeCount());
System.out.println("HttpClient SelectorManager threads are still running and will never be cleaned up!");
// Force GC to confirm threads are not cleaned up
while (true) {
Thread.getAllStackTraces().keySet().stream()
.filter(t -> t.getName().contains("HttpClient") && t.getName().contains("SelectorManager"))
.forEach(t -> System.out.println(" - " + t.getName()));
System.gc();
Thread.sleep(1000);
System.out.println("Thread count after GC: " + Thread.activeCount());
}
}
}
- Dominant language
- Java
- Stars
- 3.7k
- Forks
- 1.1k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 9
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.
More from modelcontextprotocol/java-sdk
-
area/transport bug P2
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
modelcontextprotocol/java-sdk#1136 ·
-
area/client bug P2
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
modelcontextprotocol/java-sdk#1124 · 1 comment ·
-
ServerCapabilities.logging is added unconditionally, overriding the caller's explicit capabilities Openbug P2 ready for work
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
modelcontextprotocol/java-sdk#1086 · 1 comment ·
-
enhancement good first issue P3
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
modelcontextprotocol/java-sdk#1067 ·
-
bug P2 ready for work
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
modelcontextprotocol/java-sdk#898 · 1 comment ·
All issues in modelcontextprotocol/java-sdk
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
elastic/gradle-plugins#157 ·
-
enhancement Tools
Difficulty 1/5 Under an hour Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
apache/rocketmq-dashboard#5008 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
DETECT_PARAMETER_NAMES=false silently disables @ConstructorProperties-based Creator detection too Open
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
FasterXML/jackson-databind#6229 ·