Skip to content

MCP: local servers are spawned per session and never released when a session ends #51249

Closed as duplicate

Description

@4Liberty

Summary

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

  1. Configure 3-5 local MCP servers.
  2. Use the app continuously for a few hours, opening many sessions (agent-style traffic: dozens of
    sessions per hour, each connecting its MCP tools).
  3. 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.

Activity

  1. dajiaohuang commented on Sep 25, 2026

    @dajiaohuang

    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.

  2. ggbdpq commented on Oct 2, 2026

    @ggbdpq

    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.

  3. ggbdpq commented on Oct 2, 2026

    @ggbdpq

    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.

  4. holny commented on Oct 5, 2026

    @holny
    Contributor

    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

  5. GaganDeep-007 commented on Oct 14, 2026

    @GaganDeep-007

    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.

  6. patrykkopycinski commented on Oct 27, 2026

    @patrykkopycinski

    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):

    ps -eo ppid=,rss= | awk -v p=$(sed -nE 's/.*"pid": ?([0-9]+).*/\1/p' ~/.local/state/opencode/service.json) '$1==p {c++; s+=$2} END {printf "children=%d rss=%.2fGB\n", c, s/1048576}'

    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).

  7. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    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.

  8. opencodejr commented on Oct 29, 2026

    @opencodejr
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions