Skip to content

serve: per-request instances are never disposed - MCP child processes accumulate until memory exhaustion #47727

Closed as not planned

Description

@ljieyao

Description

opencode serve creates a new Instance for every API request that carries a ?directory= parameter, and these instances — along with the MCP server processes they spawn — are never disposed or garbage-collected. Under a client that polls several project directories (e.g. OpenChamber polling session status per directory), child processes accumulate unboundedly until the host exhausts memory.

Observed on a 31GB host: 43 registered project directories × 7 enabled global MCPs → ~900 MCP child processes (~110–200MB each) plus an ~8GB serve heap → 26.9GB RSS → swap thrash → all API requests time out. The log shows "creating instance" every 5–10 seconds continuously (21k+ lines over time), cycling the same directories.

Even after trimming to 2 enabled global MCPs and 26 directories, a freshly started serve accumulates ~185 MCP child processes within minutes — the accumulation itself doesn't stop, it only slows.

Steps to reproduce

  1. Start opencode serve --hostname 0.0.0.0 --port <port> (v1.18.29, Linux)
  2. Enable a couple of global MCPs in opencode.json (e.g. an npm exec-based one)
  3. Poll any endpoint with distinct ?directory= values every few seconds (this mimics OpenChamber's status polling):
    while true; do
      curl -s -H "Authorization: Bearer ..." \
        "http://127.0.0.1:<port>/session/status?directory=/some/repo"
      sleep 5
    done
  4. Watch the serve process tree: ps --ppid $(pgrep -f 'opencode serve') — the child count and RSS grow without bound; nothing is ever reaped.

Actual vs expected

  • Actual: one instance + full MCP fleet spawned per request cycle; processes accumulate forever → memory exhaustion.
  • Expected: instances reused per directory (or disposed when idle), and MCP child process trees terminated when an instance is disposed.

OpenCode version

1.18.29 (linuxbrew install)

Operating System

Linux x86_64 (Ubuntu), headless opencode serve behind the OpenChamber web/desktop client.

Additional context

Workarounds we had to apply locally: trimming global MCPs to the bare minimum, a systemd MemoryMax cap, a watchdog that restarts the service when MCP child count > 250 or cgroup memory > 20GB, and a nightly scheduled restart. Happy to share more logs/metrics if useful.

Activity

  1. github-actions commented on Sep 7, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is distinct (e.g., the per-?directory=-request instance creation path is a different code path from the reconnect leak in #46035), please clarify that in a comment — it would help maintainers track the two separately.

  2. sugarless-sweet commented on Sep 12, 2026

    @sugarless-sweet

    Second confirmation: macOS + OpenChamber, opencode serve 1.18.30 (homebrew)

    Same accumulation on a different platform and client series. This is the per-request/per-connection instance path (#47727) and should be tracked separately from the browser-reconnect path (#46035), as requested.

    Environment

    • macOS (Apple Silicon), node v24.19.0 (nvm)
    • opencode 1.18.30 (homebrew), launched by OpenChamber desktop as opencode serve --hostname 127.0.0.1 --port 50905
    • Global config (~/.config/opencode/opencode.jsonc) has exactly 2 local stdio MCP servers (openspec via npx -y openspec-mcp, gitnexus via gitnexus mcp) plus one remote

    Live census (single serve PID, uptime ~4 min, idle, only one active chat)

    • serve holds 21 complete MCP sets, every process a direct child of serve (PPID = serve), zero orphans (no PPID 1), all launchers alive, one set per connection:
      • 21 × npm exec openspec-mcp (wrapper, 41–65 MB) → 21 × node …/openspec-mcp (30–54 MB)
      • 21 × gitnexus mcp (node, 47–69 MB)
    • Total = 63 MCP child processes ≈ 4 GB, plus the serve process itself at ~940 MB → ~5 GB from 50+ node processes minutes after launching the client.
    • Spawns arrived in 3 bursts (28 @ 09:53:16, 20 @ 09:53:48, 6 @ 09:55:14): one round at startup warming sets, then further rounds per connection — matching an instance materialized per connection/request and never disposed.
    • Network: OpenChamber holds ~12 ESTABLISHED TCP connections to :50905; serve also keeps ~20 loopback connections to its own port (same self-loopback shape reported in serve (1.18.25): MCP child processes accumulate on web-client reconnects until the server OOMs #46035). Every MCP child keeps a live PIPE to serve — active processes, not zombies.

    This reproduces #47727 on stable 1.18.30 with a desktop/web client that polls directories (OpenChamber), on macOS, with no TUI client attached — sets keyed per (connection/directory instance), never reused, never reaped. Happy to attach full ps trees or lsof dumps if useful.

  3. sugarless-sweet commented on Sep 12, 2026

    @sugarless-sweet

    Full evidence bundle (ps trees, complete lsof dumps, network/set census) captured live on stable 1.18.30 / macOS / OpenChamber:

    Evidence bundle: https://gist.github.com/sugarless-sweet/588effc909b71faa6d3e46c52a4e2c81

    Highlights:

    • In this brand-new session the serve process already re-materialized MCP sets and RSS reached ~1.46 GB within ~12 min.
    • 2 complete sets = 6 direct children (npm exec openspec → node openspec, gitnexus), zero orphans, all launchers alive. The 2nd set appeared later (etime 13:31 vs 13:48) → per-connection/request materialization.
    • Caught a transient extra node openspec-mcp spawn (2 MB) mid-census — accumulation was actively ongoing during sampling, not a one-time warm-up.
    • Network: serve holds 5 loopback self-conns to its own port; OpenChamber holds 5 conns → set count ≠ connection count, consistent with one set per directory/request instance never disposed (serve: per-request instances are never disposed - MCP child processes accumulate until memory exhaustion #47727).
    • Earlier session (serve 35235, since shut down) had accumulated 21 sets / 63 children / ~4 GB within minutes, plus serve at ~940 MB.

    Full ready-to-attach reproduction data in the bundle; happy to add periodic samplings to show the growth curve if it helps.

  4. MCKLtech commented on Sep 15, 2026

    @MCKLtech

    Adding my summary from debugging this issue on my own machine:

    Environment

    • macOS 14.6.1 — MacBook Pro Mac15,7, Apple M3 Pro (12-core), 18 GB RAM
    • OpenCode 1.18.31 (bundled in OpenChamber 1.23.2); Node v22.22.3 / npm 10.9.8; Homebrew 6.0.22
    • Plugin: oh-my-opencode-slim
    • MCPs — local/stdio: airbnb, brave-search, skills-radar; remote: crawl4ai, n8n, treg

    Symptom
    One opencode serve process accumulates MCP child processes without bound until the machine swaps. Only local (stdio) MCPs leak; remote MCPs contribute no child processes.

    Evidence (single serve pid, ~30 min session)

    • brave-search: 14 → 48 → 66
    • mcp-remote (stdio→HTTP bridge): 14 → 48 → 66
    • custom mcp server: 7 → 33
    • ≈ 90 child processes, ~1 GB RSS; system swap grew 1 GB → 6.5 GB

    Key points

    1. It is per instance (directory), not per agent — correlates with repeated message="creating instance" directory=… lines; each instance initialises a full MCP fleet that is never disposed.
    2. Not MCP-specific — reproduced identically with a stdio server and the same server behind mcp-remote, and with brave-search/airbnb we never touched.
    3. OpenChamber 1.22.1 amplified it via an auto-reconnect plugin (fixed in 1.23.1 / OpenChamber#3350), but the leak persists on 1.23.2 + OpenCode 1.18.31.
    4. No config knob (reuse / idle-timeout / max-instances / dispose) appears to exist.

    Impact
    Unbounded RSS + swap growth on an 18 GB machine; only manual reaping or restart helps. Happy to run diagnostics or test a patch.

  5. AceRothstein71 commented on Sep 17, 2026

    @AceRothstein71

    The per-directory instance cache in packages/opencode/src/project/instance-store.ts was unbounded, so serve processes visiting many directories accumulated instances and their bootstrap resources (MCP children). A fix now bounds the cache to 16 instances, evicting the oldest completed entry and refreshing recency on access; in-flight boots are never evicted. Regression test included. The fix is on the branch of PR #48638.

  6. AceRothstein71 commented on Sep 17, 2026

    @AceRothstein71

    Following the branch audits, the cache bound was reverted in 4551204c before merge: LRU eviction by load time can dispose an instance a long-running session still holds (no refcount), load awaiters are unprotected during concurrent eviction, a reload racing disposeContext can destroy the new generation's resources, and eviction can lead to a double dispose.

    Requirements for re-landing: (1) in-use guard or refcount around provide, (2) awaiter protection during eviction, (3) serialize dispose vs reload per directory, (4) fix the double-dispose after eviction, (5) regression tests covering all four.

  7. AceRothstein71 commented on Sep 17, 2026

    @AceRothstein71

    We're reverting the cache bound for now based on a full audit: evicting a completed entry can dispose a directory that is still in use (a long-running session holds the context without re-touching the store; once more than a bound's worth of directories have been loaded, the next load evicts it mid-flight), and the dispose path races reload() (async disposers can run against a freshly swapped entry, and a second identity guard can leave a dead cached entry).

    To re-land safely it needs: an in-use refcount or lease around provide() and load awaiters, serialization between dispose and reload per directory, a double-dispose guard, and tests that evict while a borrower is active.

  8. PVLPM commented on Sep 20, 2026

    @PVLPM

    Confirming this on Windows v2.0.11 — accumulation also happens without any ?directory= polling, through plain MCP reconnects, and on Windows it has a very visible side effect.

    Environment: OpenCode Desktop 2.0.11 (CLI opencode-cli.exe serve --service), Windows 11, 4 local stdio MCP servers configured via npx (@magicuidesign/mcp, @playwright/mcp, shadcn mcp, and codegraph serve --mcp). Normal interactive use, no opencode serve behind a polling client.

    After a few hours, process counts:

    cmd.exe           81
    node.exe         105
    opencode-cli.exe   1
    

    Every one of those cmd.exe traces back to the single serve --service process. Grouping the 81 cmd.exe by their direct parent:

    direct parent count launcher
    opencode-cli.exe 12 cmd.exe /d /s /c "npx -y @magicuidesign/mcp@latest"
    opencode-cli.exe 12 cmd.exe /d /s /c "npx -y @playwright/mcp@latest --config <path>"
    opencode-cli.exe 12 cmd.exe /d /s /c "codegraph serve --mcp"
    opencode-cli.exe 9 cmd.exe /d /s /c "npx shadcn@latest mcp"
    node.exe (npx-cli.js) 13 / 12 / 9 inner cmd.exe /d /s /c <pkg-bin>

    So each local MCP server costs two cmd.exe and two node.exe, and the same four servers are alive up to 12 times concurrently. Nothing is ever reaped.

    Two separate problems are visible in this one tree:

    1. The leak — the process tree is not terminated on MCP reconnect/disconnect; the counts only grow.
    2. Windows-visible console windows — those intermediates are console processes. Because they are never reaped they don't just flash for 40–150 ms, they stack up on the desktop as real persistent windows (81 of them, all titled cmd.exe), one per spawn attempt.

    Getting anywhere near #47727's numbers doesn't even require a polling client on Windows — the churn alone gets you there. Happy to dump the full Win32_Process table (PID / ParentProcessId / CommandLine) if it helps.

  9. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions