Skip to content

Conversation

halter73
Copy link
Contributor

@halter73 halter73 commented Aug 12, 2025

This PR replaces #677. The key difference between this PR and the old one is that extra idle sessions are closed immediately before starting new ones instead of waiting for the IdleTrackingBackgroundService to clean up the next time the periodic timer fires. To achieve this, I made the ConcurrentDictionary tracking the stateful StreamableHttpSessions a singleton service that's also responsible for pruning idle sessions. StreamableHttpSession was previously named HttpMcpSession<TTransport>, but it's no longer shared with the SseHandler

You can look at the description of #677 to see the consequences of creating too many new sessions without first closing and unrooting a corresponding number of idle sessions. The tl;dr is that overallocating could lead to thread pool starvation as hundreds of threads had to wait on the GC to allocate heap space. This thread pool starvation created a vicious cycle because it prevented the IdleTrackingBackgroundService from unrooting the idle sessions causing more of them to get promoted and creating more work for the GC.

In order to reduce contention, I reuse the sorted _idleSessionIds list to find the most idle session to remove next. This list only gets repopulated every 5 seconds on a background loop, or if we run out of new idle sessions to close to make room for new ones. This isn't perfect, because sessions may briefly become active while siting in the _idleSessionIds list, but not get resorted. However, this is only a problem if the server is at the MaxIdleSessionCount, and that every idle session that was less recently active during the last sort has already been closed. Considering a sort should happen at least every 5 seconds when sessions are pruned, I think this is a fair tradeoff to reduce global synchronization on session creation (at least when under the MaxIdleSessionCount) and every time a session becomes idle.

In my testing on a with 16core/64GB VM, a 100,000 idle session limit (the old default) caused the server process to consume 2-3 GB of memory according to Task Manager and limited new session creation rate to about 60 sessions/second after reaching the MaxIdleSessionCount. At a 10,000 idle session limit (the new default), the process memory usage dropped to about 300MB session creation rate increased to about 900 sessions/second. And at the even lower 1,000 idle session limit, the process memory usage dropped further to about 180MB the session creation rate increased again to about 5,000 sessions/second. All of these numbers are stable over repeated runs after having reached the MaxIdleSessionCount.

MaxIdleSessionCount = 10,000 (New default)

$ ./wrk -t32 -c256 -d15s http://172.20.240.1:3001/ -s scripts/mcp.lua
Running 15s test @ http://172.20.240.1:3001/
  32 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   260.42ms   24.53ms 342.05ms   77.48%
    Req/Sec    31.61     11.99   110.00     83.07%
  14737 requests in 15.07s, 5.04MB read
Requests/sec:    977.96
Transfer/sec:    342.43KB

MaxIdleSessionCount = 100,000 (Old default)

$ ./wrk -t32 -c256 -d15s http://172.20.240.1:3001/ -s scripts/mcp.lua --timeout 15s
Running 15s test @ http://172.20.240.1:3001/
  32 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.76s     1.68s    7.47s    56.71%
    Req/Sec     9.08     13.85    79.00     89.39%
  917 requests in 15.05s, 321.01KB read
Requests/sec:     60.92
Transfer/sec:     21.33KB

MaxIdleSessionCount = 1,000 (Lower than default)

$ ./wrk -t32 -c256 -d15s http://172.20.240.1:3001/ -s scripts/mcp.lua
Running 15s test @ http://172.20.240.1:3001/
  32 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    46.52ms    9.40ms 127.71ms   85.46%
    Req/Sec   172.64     31.54   574.00     76.85%
  82981 requests in 15.08s, 28.38MB read
Requests/sec:   5501.70
Transfer/sec:      1.88MB

Single Session Tool Call

The MaxIdleSessionCount has no apparent affect on this test, and I wouldn't expect it to, since we still look up existing sessions the same way we did previously.

$ ./wrk -t32 -c256 -d15s http://172.20.240.1:3001/ --timeout=15s -s scripts/mcp.lua <session-id>
Running 15s test @ http://172.20.240.1:3001/
  32 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     9.31ms   14.79ms 370.81ms   96.89%
    Req/Sec     1.05k   179.55     3.23k    78.45%
  503104 requests in 15.10s, 172.05MB read
Requests/sec:  33319.09
Transfer/sec:     11.39MB

The main thing is disposing pruned sessions outside the lock. This
improved session creation performance when at max idle sessions
by about 10% at a 10,000 idle session max and 100% at a 1,000 max
@halter73 halter73 merged commit b067261 into main Aug 18, 2025
8 of 10 checks passed
@halter73 halter73 deleted the halter73/prune-on-add branch August 18, 2025 18:04
microsoft-github-policy-service bot pushed a commit to Azure/bicep that referenced this pull request Aug 25, 2025
…7897)

Updated
[ModelContextProtocol](https://github.com/modelcontextprotocol/csharp-sdk)
from 0.3.0-preview.2 to 0.3.0-preview.4.

<details>
<summary>Release notes</summary>

_Sourced from [ModelContextProtocol's
releases](https://github.com/modelcontextprotocol/csharp-sdk/releases)._

## 0.3.0-preview.4

## What's Changed
* Respect HandleResponse() and SkipHandler() calls in
OnResourceMetadataRequest by @​halter73 in
modelcontextprotocol/csharp-sdk#607
* UnreferenceDisposable made slimmer by @​Scooletz in
modelcontextprotocol/csharp-sdk#627
* IdleTracking uses lists instead of SortedSet by @​Scooletz in
modelcontextprotocol/csharp-sdk#629
* Fix ResourceLinkBlock deserialization by adding missing "name" case by
@​Copilot in modelcontextprotocol/csharp-sdk#645
* Add an in-memory transport sample by @​stephentoub in
modelcontextprotocol/csharp-sdk#664
* Remove 'Sse' from AspNetCoreSseServer sample name by @​halter73 in
modelcontextprotocol/csharp-sdk#665
* Fix NotSupportedException when returning IEnumerable<ContentBlock> by
@​Copilot in modelcontextprotocol/csharp-sdk#675
* Enhance HTTP and MCP session logging by @​theojiang25 in
modelcontextprotocol/csharp-sdk#608
* Remove special-casing of string enumerables in McpServerTool by
@​stephentoub in
modelcontextprotocol/csharp-sdk#699
* Prune idle sessions before starting new ones by @​halter73 in
modelcontextprotocol/csharp-sdk#701
* Add framework for conceptual docs by @​mikekistler in
modelcontextprotocol/csharp-sdk#708

## New Contributors
* @​Scooletz made their first contribution in
modelcontextprotocol/csharp-sdk#627
* @​Copilot made their first contribution in
modelcontextprotocol/csharp-sdk#645
* @​theojiang25 made their first contribution in
modelcontextprotocol/csharp-sdk#608

**Full Changelog**:
modelcontextprotocol/csharp-sdk@v0.3.0-preview.3...v0.3.0-preview.4

## 0.3.0-preview.3

## What's Changed
* Enable netfx testing. by @​eiriktsarpalis in
modelcontextprotocol/csharp-sdk#588
* fix: Prevent crash when Options.ResourceMetadata is null but handled
by event by @​DavidParks8 in
modelcontextprotocol/csharp-sdk#603
* Update to M.E.AI 9.7.0 by @​stephentoub in
modelcontextprotocol/csharp-sdk#602
* Ensure IsExternalInit is type forwarded on NET builds by @​stephentoub
in modelcontextprotocol/csharp-sdk#619
* Flow ExecutionContext with JsonRpcMessage by @​halter73 in
modelcontextprotocol/csharp-sdk#616
* Update MEAI version and add regression test for #​601. by
@​eiriktsarpalis in
modelcontextprotocol/csharp-sdk#628

## New Contributors
* @​DavidParks8 made their first contribution in
modelcontextprotocol/csharp-sdk#603

**Full Changelog**:
modelcontextprotocol/csharp-sdk@v0.3.0-preview.2...v0.3.0-preview.3

Commits viewable in [compare
view](modelcontextprotocol/csharp-sdk@v0.3.0-preview.2...v0.3.0-preview.4).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ModelContextProtocol&package-manager=nuget&previous-version=0.3.0-preview.2&new-version=0.3.0-preview.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/17897)

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants