You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
MCP: local servers are spawned per session and never released when a session ends #51249
The background service starts a fresh local MCP server set per session/directory and does not
release it when that session ends. On a host with several local stdio servers configured, the MCP
process count grows monotonically for the lifetime of the service and is only cleared by a service
restart.
Environment
OpenCode desktop v2.0.15, Windows 11, shared background service
Global config with several local stdio MCP servers (one Node wrapper, two uvx/Python servers, one
prepackaged executable) and two remote servers
Steps
Configure 3-5 local MCP servers.
Use the app continuously for a few hours, opening many sessions (agent-style traffic: dozens of
sessions per hour, each connecting its MCP tools).
Count the spawned MCP server processes over time.
Actual
Each session/directory spawns its own set; nothing from an ended session exits.
Measured ~46 sets (~230 live processes, about five local servers per set) after roughly two hours
of continuous use, and ~121 sets (~600 processes) across a longer service lifetime earlier the
same day. The count only returned to zero after the service restarted.
Remote MCP servers show no local processes, as expected; the growth is entirely local stdio
servers.
Expected
MCP servers should be closed when their session ends, or shared per server instead of spawned per
session, so the live count is bounded by concurrent sessions rather than by sessions opened over
the service's lifetime.
Impact
Hundreds of idle processes retain memory and scheduler pressure on a busy host, slowing unrelated
work. Operators currently work around it with an external reaper or periodic service restarts.
The current MCP service caches clients per project directory and closes their transports when that directory instance is disposed. In this reproduction, do the project directories remain open after sessions end, and should local servers be shared until directory disposal or stopped after each session? That lifecycle boundary determines whether a fix should preserve the current per-directory design or change it.
Taking this one. Plan: first confirm the actual lifecycle boundary dajiaohuang outlined — whether directory instances are disposed in the shared background service while their cached MCP clients keep processes alive — then pick the smallest fix that bounds live local server processes by concurrent sessions rather than session history. Will credit the analysis here and follow up with a PR.
Update: after tracing the code I'm stepping back from this one — the analysis confirms the boundary question @dajiaohuang raised is exactly the blocker.
What the code does today (dev):
MCP clients (and their stdio processes) are cached per project directory in the MCP service's InstanceState, and the finalizer that kills the process tree is complete and correct (packages/opencode/src/mcp/index.ts, Effect.addFinalizer in the state init).
The directory-keyed instance cache in InstanceStore (packages/opencode/src/project/instance-store.ts) is an unbounded Map with no idle eviction; disposal only happens via the explicit dispose APIs or the per-request markInstanceForDisposal used by a couple of endpoints.
Session end triggers no instance disposal, so a directory's MCP set intentionally lives until the instance is disposed — "shared per directory" is the current design.
So the leak isn't inside the MCP module; it's that instance lifetime is unbounded, and fixing it well means choosing the eviction boundary (idle TTL in InstanceStore vs client-driven dispose), which affects every per-instance state, not just MCP. That's a maintainers-level design call I shouldn't make unilaterally, so I'm releasing my claim. The trace above should be reusable for whoever picks the design decision up.
I'll take this. The lifecycle gap is in the session-scoped MCP instance teardown: clients are cached per project directory inside the MCP service, and the cache cleanup only runs when the whole directory instance disposes — an individual session ending never touches it. On the shared background service that means every visited directory accumulates its own set of stdio servers for the life of the process.
Plan: trace the MCP client cache against the session/directory lifecycle to pin the exact ownership boundary, then wire client release into the path that already fires when a session closes. Covered by a test that opens two sessions in different directories, closes one, and asserts its stdio servers are gone.
Concrete Linux impact case: on an 11GB Arch Linux machine, 3 concurrent opencode TUI instances each spawned their own local MCP servers (playwright, firecrawl, postgres, plus billion-context plugin). That's ~15 node processes at 90-115MB each, plus 3 opencode binaries at ~900MB RSS each. Total memory demand exceeded physical RAM, driving swap to 8.2GB/23GB. The system froze under swap thrashing (whole desktop unresponsive for minutes), and two opencode instances then crashed with SIGABRT (see #55459).
The MCP process fan-out per instance is the dominant memory multiplier here. Lazy spawn (#53028) or shared MCP server pools would prevent this class of freeze on memory-constrained hosts.
Corroborating on macOS (desktop v2.1.4, arm64, shared serve --service) with a data point that goes a step beyond per-directory accumulation: pairs multiply per session within a directory, not just per visited directory.
Measured on the live service (two local stdio servers configured globally — one Node ~36 MB, one Python ~44 MB per process):
15 sessions through the service (a handful of distinct directories, mostly 1-3 in active rotation) → 62 server pairs ≈ 4 pairs per session. Sessions that exercised tool-calling subagents spawned multiple pairs each; a plain session spawns at least one.
Growth is strictly monotonic — counts never decrease as sessions end; only a service restart releases the children.
133 children / 2.97 GB combined RSS on a 49-minute-old service; the replacement service re-accumulated 136 children / 2.96 GB within 30 minutes.
Downstream effect: on a 48 GB host this repeatedly pushed free RAM under 250 MB with ~32 GB of swap in use, and the desktop app churned 7 service restarts inside one hour under that pressure.
Why this matters for the fix in #53366 (and the InstanceStore eviction discussion above): cleanup keyed on "a location's last session is deleted" addresses the directory-lifetime case, but the observed ~4x per-session duplication within a single live directory means the per-directory client cache itself fans out per session (or per agent/subagent scope). Even with last-session-delete cleanup, a long-lived directory with continuous session churn will keep accumulating pairs until its last session goes away. Bounding live stdio processes by concurrent sessions (dedupe on acquire, release on session close) would cover both shapes.
One-command repro for anyone measuring (count + RSS of the service's direct children):
Related reports of the same symptom class, for deduplication: #50363 (orphaned npx servers after shutdown), #52965 (duplicate servers, ~6 GB), #46035 (serve OOM on client reconnects), #31554 (2-4 duplicates per server).
Thanks for the detailed measurements. I think this is a duplicate of #50363: under the background service (opencode serve --service), each session starts its own set of local stdio MCP servers, and they keep running after the session ends until the service restarts. Others have confirmed it there on Linux and Windows up to v2.0.24.
Possible duplicates and closely related issues, ordered from most to least likely:
Related pull requests: #53366 (stops a location's stdio MCP servers when its last session is deleted; it currently references this issue, so it's worth linking it to #50363 too) and #53046 (releases connections that were opened only to discover tools).
Your Windows 2.0.15 numbers (~46 sets / ~230 processes after two hours) would be a useful addition to #50363. The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
Closing this as a duplicate of #50363, since there's been no reply saying how this differs. That issue covers the same problem: under the background service (opencode serve --service), each session starts its own set of local stdio MCP servers, and they keep running after the session ends until the service restarts.
Please follow #50363 for updates. Your Windows 2.0.15 measurements (~46 sets / ~230 processes after two hours, ~600 over a longer run) would be useful there. PR #53366, which currently references this issue, is the one to watch for a fix. If you think this is a different problem, reply here and it can be reopened.
Activity
dajiaohuang commentedon Sep 25, 2026
The current MCP service caches clients per project directory and closes their transports when that directory instance is disposed. In this reproduction, do the project directories remain open after sessions end, and should local servers be shared until directory disposal or stopped after each session? That lifecycle boundary determines whether a fix should preserve the current per-directory design or change it.
ggbdpq commentedon Oct 2, 2026
Taking this one. Plan: first confirm the actual lifecycle boundary dajiaohuang outlined — whether directory instances are disposed in the shared background service while their cached MCP clients keep processes alive — then pick the smallest fix that bounds live local server processes by concurrent sessions rather than session history. Will credit the analysis here and follow up with a PR.
ggbdpq commentedon Oct 2, 2026
Update: after tracing the code I'm stepping back from this one — the analysis confirms the boundary question @dajiaohuang raised is exactly the blocker.
What the code does today (
dev):InstanceState, and the finalizer that kills the process tree is complete and correct (packages/opencode/src/mcp/index.ts,Effect.addFinalizerin the state init).InstanceStore(packages/opencode/src/project/instance-store.ts) is an unboundedMapwith no idle eviction; disposal only happens via the explicit dispose APIs or the per-requestmarkInstanceForDisposalused by a couple of endpoints.So the leak isn't inside the MCP module; it's that instance lifetime is unbounded, and fixing it well means choosing the eviction boundary (idle TTL in
InstanceStorevs client-driven dispose), which affects every per-instance state, not just MCP. That's a maintainers-level design call I shouldn't make unilaterally, so I'm releasing my claim. The trace above should be reusable for whoever picks the design decision up.holny commentedon Oct 5, 2026
I'll take this. The lifecycle gap is in the session-scoped MCP instance teardown: clients are cached per project directory inside the MCP service, and the cache cleanup only runs when the whole directory instance disposes — an individual session ending never touches it. On the shared background service that means every visited directory accumulates its own set of stdio servers for the life of the process.
Plan: trace the MCP client cache against the session/directory lifecycle to pin the exact ownership boundary, then wire client release into the path that already fires when a session closes. Covered by a test that opens two sessions in different directories, closes one, and asserts its stdio servers are gone.
Fixes #51249
GaganDeep-007 commentedon Oct 14, 2026
Concrete Linux impact case: on an 11GB Arch Linux machine, 3 concurrent opencode TUI instances each spawned their own local MCP servers (playwright, firecrawl, postgres, plus billion-context plugin). That's ~15 node processes at 90-115MB each, plus 3 opencode binaries at ~900MB RSS each. Total memory demand exceeded physical RAM, driving swap to 8.2GB/23GB. The system froze under swap thrashing (whole desktop unresponsive for minutes), and two opencode instances then crashed with SIGABRT (see #55459).
The MCP process fan-out per instance is the dominant memory multiplier here. Lazy spawn (#53028) or shared MCP server pools would prevent this class of freeze on memory-constrained hosts.
patrykkopycinski commentedon Oct 27, 2026
Corroborating on macOS (desktop v2.1.4, arm64, shared
serve --service) with a data point that goes a step beyond per-directory accumulation: pairs multiply per session within a directory, not just per visited directory.Measured on the live service (two local stdio servers configured globally — one Node ~36 MB, one Python ~44 MB per process):
Why this matters for the fix in #53366 (and the
InstanceStoreeviction discussion above): cleanup keyed on "a location's last session is deleted" addresses the directory-lifetime case, but the observed ~4x per-session duplication within a single live directory means the per-directory client cache itself fans out per session (or per agent/subagent scope). Even with last-session-delete cleanup, a long-lived directory with continuous session churn will keep accumulating pairs until its last session goes away. Bounding live stdio processes by concurrent sessions (dedupe on acquire, release on session close) would cover both shapes.One-command repro for anyone measuring (count + RSS of the service's direct children):
Related reports of the same symptom class, for deduplication: #50363 (orphaned npx servers after shutdown), #52965 (duplicate servers, ~6 GB), #46035 (
serveOOM on client reconnects), #31554 (2-4 duplicates per server).opencodejr commentedon Oct 28, 2026
Thanks for the detailed measurements. I think this is a duplicate of #50363: under the background service (
opencode serve --service), each session starts its own set of local stdio MCP servers, and they keep running after the session ends until the service restarts. Others have confirmed it there on Linux and Windows up to v2.0.24.Possible duplicates and closely related issues, ordered from most to least likely:
serve.Related pull requests: #53366 (stops a location's stdio MCP servers when its last session is deleted; it currently references this issue, so it's worth linking it to #50363 too) and #53046 (releases connections that were opened only to discover tools).
Your Windows 2.0.15 numbers (~46 sets / ~230 processes after two hours) would be a useful addition to #50363. The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
opencodejr commentedon Oct 29, 2026
Closing this as a duplicate of #50363, since there's been no reply saying how this differs. That issue covers the same problem: under the background service (
opencode serve --service), each session starts its own set of local stdio MCP servers, and they keep running after the session ends until the service restarts.Please follow #50363 for updates. Your Windows 2.0.15 measurements (~46 sets / ~230 processes after two hours, ~600 over a longer run) would be useful there. PR #53366, which currently references this issue, is the one to watch for a fix. If you think this is a different problem, reply here and it can be reopened.