Windows: codex mcp-server / app-server and their child MCP servers are never reaped (accumulate to hundreds over time)

Open 💬 6 comments Opened Jun 15, 2026 by yonghyeon-dev
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

On Windows, using Codex through its MCP-server path (codex mcp-server, which a host like Claude Code launches) leaks processes: every request spawns a new codex app-server, which in turn spawns the MCP servers from ~/.codex/config.toml — and none of them are reaped when the request/session ends. Over a few days of normal use they accumulate into the hundreds and noticeably degrade the machine.

codex exec (one-shot) does not have this problem — it spawns the same children but cleans them up on normal exit. The leak is specific to the persistent mcp-server/app-server path on Windows.

Environment

  • OS: Windows 11
  • codex-cli: 0.139.0 (model gpt-5.5)
  • Configured MCP servers in ~/.codex/config.toml: playwright, comfyui, node_repl (any configured stdio MCP servers reproduce this)

Observed behavior (controlled A/B, measured with Get-CimInstance Win32_Process)

  • Fresh reboot baseline: 1 codex mcp-server, 0 child MCP processes.
  • One request through the MCP server: +1 codex app-server, and +1 of each configured MCP server (each as a cmd → npx → node chain). After the request completes, none of these exit.
  • After a few days / many sessions: ~800+ lingering node / cmd / codex processes. Parent analysis: only ~3 were true orphans; ~835 still had live parents — piled up under 2 long-lived codex app-server instances (52–56h old), i.e. children accumulating under persistent app-servers rather than being cleaned up.

Contrast: codex exec is clean

Running the equivalent work via codex exec --ephemeral ... - spawns the same MCP children during the run, but all of them return to baseline on normal exit (verified). So the runtime can tear them down — the persistent mcp-server/app-server path on Windows doesn't.

Possibly related

codex app-server daemon <...> reports: app-server daemon lifecycle is only supported on Unix platforms. This suggests Windows app-server lifecycle management is less complete, which may be why child trees aren't reaped on Windows.

Repro

  1. On Windows, configure one or more stdio MCP servers in ~/.codex/config.toml.
  2. Drive Codex through codex mcp-server (e.g. via an MCP host) and issue several requests.
  3. Watch codex.exe, node.exe, and the MCP server processes via Task Manager / Get-CimInstance Win32_Process — they accumulate and never exit.

Impact

Resource exhaustion (hundreds of processes / multiple GB) over long-lived sessions on Windows.

Suggested direction

Reap each app-server and its child MCP process tree when the request/session ends (or on mcp-server shutdown), including a Windows tree-kill (e.g. taskkill /pid <pid> /t /f) since Windows does not cascade-terminate child processes on parent exit.

_(Searched existing issues for duplicates; didn't find an obvious match.)_

View original on GitHub ↗

6 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #26773
  • #26984
  • #27162
  • #26869

Powered by Codex Action

tangweigang-jpg · 1 month ago

The useful split in this repro is codex exec clean vs codex mcp-server leaking on the same configured stdio MCP servers. That makes this look less like an individual MCP server shutdown bug and more like ownership of the child process tree in the persistent app-server path.

One acceptance test that would make the fix hard to regress: start codex mcp-server on Windows with two trivial stdio MCP servers, send one request through an MCP host, wait for the request/session to finish, then assert that the descendant codex app-server, cmd, npx, and node processes return to the pre-request count. I would also keep the codex exec --ephemeral case in the same test as a control, since it proves teardown is possible for the same config.

The duplicate check should probably distinguish true orphans from still-parented descendants too. The report's "~835 still had live parents" detail is important; a cleanup that only kills orphaned children would miss the main failure mode here.

NAlle33 · 1 month ago

Confirming on codex-cli 0.144.1, Windows 11, plain interactive CLI sessions (not the codex mcp-server path) — and adding a pattern that may help narrow it down.

Generation pattern per subagent/reconnect wave

With 8 stdio MCP servers in ~/.codex/config.toml, each interactive session re-spawns a complete set of all configured servers as a "generation" on what appears to be subagent/reconnect boundaries, without closing the previous generation. Measured via Get-CimInstance Win32_Process:

  • 3 instances of the same MCP server spawned within 33 seconds under one codex.exe PID
  • single sessions holding 24–31 MCP server processes after a few hours (3–4 full generations × 8 servers)
  • box-wide: 105 stale MCP server processes across 5 live codex sessions in one day (~188 node.exe total)

The abandoned generations are connected-but-silent: their stdin pipes are never closed (no EOF reaches the child), they receive no further MCP traffic, and they stay alive indefinitely. So this reproduces with live, healthy parents — it is not only an exit/reap problem; old generations are abandoned mid-session.

Server-side mitigation that works (for MCP server authors)

We patched our own stdio servers to self-terminate, which fully drains the leak for servers you control:

  1. stdin EOF/close/error → exit (covers clean parent exits)
  2. parent-PID watchdog — process.kill(ppid, 0) liveness + a creation-time identity fingerprint (covers hard kills; Windows never reaps orphans)
  3. opt-in idle-exit env (<SERVER>_IDLE_EXIT_MIN=120 set only in codex's [mcp_servers.X.env] blocks): the server exits after N minutes without any incoming MCP message, and codex transparently respawns it on next use. This is the only signal that catches the live-parent abandoned-generation class.

Third-party servers can't be patched this way, so an upstream fix (close the previous generation's pipes when spawning a replacement, and/or Job Objects with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE on Windows) would still be needed.

Related: #25015 describes what looks like the same per-subagent stack leak on Linux.

Jakepawl · 6 days ago

Confirming this reproduces on Linux, which I think matters for the windows-os label and for the "Windows app-server lifecycle is less complete" hypothesis in the original report.

Environment

  • Ubuntu 24.04.4 LTS, kernel 7.0.0-30-generic
  • codex-cli 0.147.0, node v24.7.0
  • 17 stdio MCP servers configured in ~/.codex/config.toml
  • Driven through codex mcp-server (launched by a host), several concurrent sessions

Same generation pattern as @NAlle33 reported, larger magnitude

The complete configured set is re-spawned as a "generation" without the previous one being closed. Measured from /proc:

  • 27 instances of the same MCP server kind spawned within 60 seconds under a single codex mcp-server PID
  • one codex mcp-server PID accumulating 182 child MCP servers over ~3 hours
  • box-wide peak 844 MCP-related processes, and 237 instances of one single server kind
  • currently, ~20 minutes after a full cleanup, one codex mcp-server PID is already back to 47 children

Parent analysis matches the original report

This is the detail I want to reinforce, because it defeats the obvious mitigations. Classifying every MCP process by walking PPid up to its owning session:

  • true orphans (reparented to init, or to systemd --user): 0 to 18 at any sample
  • abandoned-but-still-parented under a live codex mcp-server: hundreds

So pkill-by-orphan does nothing, and the processes are not reaped by normal parent-death cleanup. They are idle, connected-but-silent, and they get paged out rather than freed.

Impact

The abandoned generations end up holding almost entirely swap, not RSS, since they go idle immediately. On this box they drove swap from 0 to 24 GB (100%, fully exhausted) and made the machine unusable, repeatedly. Reaping the abandoned generations reliably returns 12-14 GB.

Caveats on my evidence

Being explicit about confidence, since the original report did a clean A/B and I did not:

  • These are observational measurements over a live working day, not a controlled fresh-reboot single-request A/B.
  • I have not verified the codex exec --ephemeral control case on Linux, so I cannot independently confirm the "exec is clean, mcp-server leaks" split here.
  • codex app-server processes do exist on this box, but from the desktop app and the VS Code extension; the accumulating children I measured sit under codex mcp-server PIDs.

Suggestion

Given a Linux reproduction on 0.147.0, the windows-os label is probably too narrow, and the "app-server daemon lifecycle is only supported on Unix platforms" note may be pointing away from the real cause rather than toward it. The acceptance test @tangweigang-jpg proposed would be worth running on Linux too.

Jakepawl · 5 days ago

Correction to my comment above — one figure I posted was wrong, and I want to fix it before it misleads anyone's debugging.

I said "27 instances of the same MCP server kind spawned within 60 seconds." That number came from a bug in my own measuring tool: its 60-second window compared process ages without guarding the sign, so anchoring on the oldest member made every younger one pass trivially. It was reporting group size, not burst size. The accumulation counts in my comment were direct process counts and are unaffected; only the "within 60 seconds" figure was wrong.

Re-measured with that fixed, the picture is more useful, because it splits into two distinct behaviours rather than one:

| Owner / kind | instances | max in any 60s | span | sustained rate |
|---|---|---|---|---|
| codex mcp-server A, remote-bridge server | 501 | 11 | 383 min | ~1.3/min |
| codex mcp-server B, remote-bridge server | 78 | 12 | 382 min | ~0.2/min |
| codex mcp-server A, stdio server (chrome-devtools) | 18 | 18 | 1 min | one burst |

So on Linux I see:

  1. A genuine burst on a stdio server — 18 copies of one kind inside a single minute, matching the "generation" pattern described above.
  2. A sustained drip on an mcp-remote bridge server — ~1.3 new processes per minute, held for over 6 hours, reaching 501 under one PID. This is the larger share by far: 576 of 660 abandoned processes at last count, ~9.9 GB RSS + 9.0 GB swap.

The drip is worth separating out because the obvious explanation does not hold: the remote endpoint is healthy (HTTP 200 in 88 ms when checked mid-pileup), so this is not a reconnect-retry storm against a failing server. Something is creating a new bridge process per interaction and never closing the old one.

Everything else in my previous comment stands: codex-cli 0.147.0 on Ubuntu 24.04, 844 MCP-related processes box-wide at peak, near-zero true orphans with the overwhelming majority still parented to a live codex mcp-server, and swap driven to full exhaustion.

Apologies for the bad number.

MichaelSpece · 3 days ago

Confirming the same live-parent, connected-but-idle generation retention on Linux with the interactive TUI (not codex mcp-server or Desktop).

Environment

  • Ubuntu 24.04, x86_64, kernel 6.17
  • source codex-cli 0.149.0
  • four configured local stdio MCP servers

Observed process lifecycle

  • One live TUI codex parent spawned four complete generations of the same configured server set over approximately 88 seconds.
  • Each generation contained three Python servers and one Node server: 12 Python + 4 Node children total, approximately 0.6 GiB aggregate RSS.
  • Every server remained a direct child of the live codex parent; none was reparented or orphaned.
  • Each server still had an open stdin pipe. The older generations were connected but idle rather than receiving EOF.
  • Generation start times aligned with four thread initialization/resume events. Two of those thread lifecycles produced only their initialization event and no later activity during the observation window, but their full MCP generations remained alive.

This is therefore the same abandoned-generation class described above, now reproduced in the direct interactive TUI on 0.149.0. A bounded result would retain MCP runtimes only for genuinely active threads, or detach/shut them down for inactive/completed threads and recreate them lazily when the thread resumes.