MCP server processes leak: per-thread processes never cleaned up (9+ GB RSS)

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

Bug description

Codex app-server spawns a full set of global MCP server processes for each new thread/conversation, but never kills them when threads are archived or closed. Over time, orphaned MCP processes accumulate unboundedly.

Environment

  • Codex Desktop: 0.142.3 (macOS, Apple Silicon)
  • OS: macOS 15.x, 64 GB RAM

Steps to reproduce

  1. Configure global MCP servers in ~/.codex/config.toml (e.g. playwright, davinci-resolve, node_repl)
  2. Open Codex Desktop, create ~25 threads over a session
  3. Close/archive threads as normal
  4. Check process list

Expected behavior

MCP server processes should be cleaned up when the owning thread is closed or archived. At most one set of MCP processes should exist per actively-used server.

Actual behavior

After 25 threads, observed 133 orphaned MCP processes consuming ~9.3 GB RSS:

| MCP Server | Orphaned Processes | RSS |
|---|---|---|
| playwright (npm + node) | 52 | 2.9 GB |
| davinci-resolve (python) | 26 | 3.5 GB |
| node_repl | 26 | 266 MB |
| npm exec wrappers | 29 | 2.6 GB |

All processes had PPID = app-server PID. The app-server holds them indefinitely.

Root cause

The app-server does not track which MCP processes belong to which thread, and has no lifecycle management to kill MCP processes when threads end. There is also no idle_timeout, reuse, or shared config option for MCP servers.

Workaround

  • Manually kill orphaned processes: pkill -P <app-server-pid> -f "playwright-mcp|davinci-resolve"
  • Remove rarely-used MCP servers from global config and add them per-project instead

Suggested fix

  • Track MCP process ownership per-thread
  • Kill MCP processes when the owning thread is closed/archived
  • Alternatively, add a shared or reuse mode so all threads share a single MCP process instance
  • Consider an idle_timeout option to auto-kill MCP processes after N minutes of no use

View original on GitHub ↗

14 Comments

github-actions[bot] contributor · 23 days ago

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

  • #29057
  • #29079

Powered by Codex Action

ZenAlexa · 18 days ago

Independent reproduction on macOS at a different config scale — confirming this isn't size-specific.

Environment: Codex Desktop (macOS, Apple Silicon), macOS 26.x; 24 global MCP servers configured in ~/.codex/config.toml.

Observed: a single live codex app-server process held 101 direct child processes (ppid == app-server) — roughly **5 copies of every configured server: @modelcontextprotocol/server-github ×5, server-memory ×5, server-sequential-thinking ×5, @playwright/mcp ×5, chrome-devtools-mcp ×5, gemsuite-mcp (via tsx) ×5, plus the uv-based servers (arxiv-mcp-server, semantic-scholar-mcp, reddit-*) at similar multiplicity. System-wide that was 128 node + 24 uv** processes. This matches the root cause described here exactly: the app-server spawns the full MCP fleet per thread and never reaps the prior sets.

Two amplifiers worth noting:

  1. npm exec doubling — each stdio server launched via npm exec <pkg> is two processes (the npm wrapper + the actual node), so N configured servers × M threads produces ~2·N·M processes.
  2. Config size is a multiplier — with 24 servers configured, each leaked thread cycle adds ~48+ processes. Overlapping/rarely-used servers make it much worse (I had 6 web-search servers configured, most unused).

The cost isn't only RAM — it's disk exhaustion and system-wide failure, which I think deserves emphasis alongside the RSS number:

  • Memory pressure from the leaked fleet grows swap — vm.swapusage was at ~2.5 GB used with multiple /System/Volumes/VM/swapfile* present.
  • The orphaned processes hold deleted files openlsof +L1 shows unlinked-but-open inodes pinned by the stale node processes, so that space is never reclaimed by the OS.
  • Net effect on a 460 GB APFS volume: free space silently collapses toward 0 with no corresponding visible files, eventually reaching the state described in #26869 / #25744 where the desktop looks normal but launching executables/actions fails until reboot. A reboot only "fixes" it because it kills the whole tree.

So the leak has three compounding costs: RAM → swap → APFS free space, plus held-open deleted-file space.

+1 on the suggested fixes, in priority order for the disk angle:

  • a shared/reuse mode so all threads share one MCP instance (biggest win — collapses the ×M entirely), and/or
  • per-thread ownership tracking + reap on thread close/archive, and
  • an idle_timeout to auto-kill idle MCP processes.

Happy to provide sanitized ps / lsof captures through a private channel if useful.

ryofukutani · 15 days ago

I can reproduce a similar leak on macOS with the current Codex Desktop app.

Environment:

  • Codex Desktop: 26.623.101652
  • Bundle build: 4674
  • macOS: 26.5.1 (25F80)
  • Architecture: Apple Silicon arm64
  • App server: /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled

Observed on July 6, 2026 JST under the live app-server PID:

Codex app-server PID: 82560
Codex app-server RSS: 740896 KB
node_repl count: 66, aggregate RSS: 592608 KB
delimit-mem-proxy count: 70, aggregate RSS: 63680 KB

The node_repl and MCP proxy processes are children of the Codex app-server. Their start times are spread across the session, with many alive for 1-3 hours, so they do not look like short-lived tool-call processes being cleaned up normally. They are mostly sleeping with near-zero CPU, but they keep accumulating.

This happened during a session that included repeated Browser / in-app browser recovery attempts and MCP/tool usage. I cannot prove the browser path is the only trigger, but the timestamps line up with repeated browser-control attempts.

Expected behavior:

  • per-thread or per-tool child processes should be cleaned up when the tool call/thread ends, or reused through a bounded pool.

Actual behavior:

  • node_repl and MCP proxy processes remain under app-server and accumulate across the session.

Additional note: the high-RSS Python process in my snapshot is a local delimit-memory daemon backed by mem0, not the old Delimit product itself. The leaking symptom I am reporting here is the app-server-owned child process accumulation (node_repl and MCP proxies).

mochafreddo · 14 days ago

I am seeing what appears to be the same MCP lifecycle leak, with an additional failure mode: app-server FD exhaustion leading to Too many open files (os error 24) on macOS.

Environment:

  • macOS, Apple Silicon
  • Codex Desktop app
  • ulimit -n: 256
  • launchctl limit maxfiles: observed as 65536 unlimited, but Codex-spawned shell still inherited ulimit -n = 256

Observed app-server processes:

PID 4815
PPID 4709
elapsed: 01-21:44
command: /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled
lsof line count: fd=334
PID 30519
PPID 30513
elapsed: 04-03:29
command: codex app-server --listen unix://
lsof line count: fd=175

There was also an orphaned launcher shell:

PID 30513
PPID 1
command: /bin/sh -c ... nohup codex app-server --listen unix:// ...

The GUI app-server had duplicated MCP children:

parent PID 4815:
  chrome-devtools-mcp: 9
  playwright-mcp: 10
  lightpanda mcp: 11
  node_repl: 9
  SkyComputerUseClient mcp: 11
  node ./mcp/server.bundle.mjs: 11
  node ./mcp/server.cjs --stdio: 9
  node ./mcp/server.mjs: 10
  node ./mcp/server.mjs --stdio: 10

The Unix-socket app-server also had duplicated MCP children:

parent PID 30519:
  chrome-devtools-mcp: 4
  playwright-mcp: 5
  lightpanda mcp: 5
  node_repl: 5
  SkyComputerUseClient mcp: 3
  node ./mcp/server.bundle.mjs: 5
  node ./mcp/server.cjs --stdio: 5
  node ./mcp/server.mjs: 4
  node ./mcp/server.mjs --stdio: 4

The generic ./mcp/server.* processes mapped back by cwd to plugin-bundled MCPs:

node ./mcp/server.mjs
  cwd: ~/.codex/plugins/cache/openai-curated-remote/openai-developers/1.2.3

node ./mcp/server.mjs --stdio
  cwd: ~/.codex/plugins/cache/openai-curated-remote/codex-security/0.1.10

node ./mcp/server.cjs --stdio
  cwd: ~/.codex/plugins/cache/openai-curated-remote/data-analytics/...

node ./mcp/server.bundle.mjs
  cwd: ~/.codex/plugins/cache/openai-curated-remote/creative-production/0.1.23

This eventually reproduced inside Codex as:

CreateProcess failed:
Too many open files (os error 24)

So this is not only memory/process accumulation; with a low inherited RLIMIT_NOFILE, the app-server can become unable to spawn new commands.

liyanqing90 · 11 days ago

Independent severe reproduction on macOS, adding Jetsam-level evidence to this MCP lifecycle leak. The system became unresponsive and required a manual restart.

Environment

  • Desktop bundle: ChatGPT/Codex 26.707.31123 (bundle build 5042)
  • Bundled app-server: codex-cli 0.144.0-alpha.4
  • macOS 26.5.1 (25F80)
  • Mac mini, Apple M4, 16 GB RAM

macOS Jetsam evidence

macOS wrote two JetsamEvent reports 5m56s apart. The reports use a 16,384-byte page size. The values below are direct calculations from the reports (GiB = pages × 16,384 / 2^30):

| Snapshot | All node processes | node in Codex coalition | All-node resident pages | Codex coalition processes | Codex coalition resident pages | Free memory |
|---|---:|---:|---:|---:|---:|---:|
| 06:20:05 | 760 | 755 | 1,662,363 = 25.37 GiB | 799 | 1,895,040 = 28.92 GiB | 5,445 pages = 85.1 MiB |
| 06:26:01 | 843 | 842 | 1,778,409 = 27.14 GiB | 881 | 2,034,698 = 31.05 GiB | 2,246 pages = 35.1 MiB |

In 5m56s:

  • node count increased by 83
  • Codex-coalition process count increased by 82
  • all-node resident memory increased by 1.77 GiB
  • Codex-coalition resident memory increased by 2.13 GiB

At the second snapshot, the system compressor occupied 9.40 GiB and represented 50.99 GiB of uncompressed, system-wide pages. This is not all attributable to Codex, but it shows the severity of the resulting system pressure.

A separate node diagnostic report identifies the ownership chain as:

responsibleProc = ChatGPT
coalitionName   = com.openai.codex
parentProc      = node

No kernel-panic report was present. The machine was manually restarted at 06:32 after the UI became unusable.

Post-restart reproduction

After restart, read-only process inspection again showed repeated app-server-owned batches containing:

npm exec @playwright/mcp@latest
npm exec chrome-devtools-mcp@latest
npm exec xcodebuildmcp@latest mcp
node ./scripts/start-mcp.mjs
node ./mcp/server.mjs
.../cua_node/bin/node_repl

Their start times appeared in repeated batches during tool-enabled thread activity, and earlier batches remained alive. This observation supports the lifecycle leak reported here, but it does not distinguish whether each new batch is keyed to a thread, subagent, tool-host lifecycle, or another app-server boundary.

Impact

On a 16 GB Mac this progressed from an application leak to a system-wide outage. Quitting/restarting the app-server is currently the only reliable way I found to release the accumulated process tree.

I have intentionally omitted project names, usernames, local paths, and raw session logs. I can provide sanitized excerpts or the aggregation queries through a private channel if useful.

DongwonTTuna · 9 days ago

Adding an independent Ubuntu/Linux reproduction. This issue is still reproducible with codex-cli 0.144.1, and in this run it progressed to complete unified-exec/PTY failure.

Environment

  • Ubuntu Linux, x86_64, long-running Codex app-server
  • App-server command: codex app-server --listen unix://
  • App-server launched by a managed systemd --user service
  • Inherited RLIMIT_NOFILE: soft 1024, hard 524288
  • Three global stdio MCP servers, each configured as:

docker exec -i -w <workspace> mcp-suite mcp-suite-stdio <lsp|codegraph|agbrowse>

The incident followed a large collaboration/subagent tree containing many completed or interrupted agents.

User-visible failure

Eventually even a trivial process could not be created:

Failed to create unified exec process: No file descriptors available (os error 24)

The PTY path failed independently:

failed to openpty ... os error 24

One attempt also reported dup of fd 992 failed. The same failure occurred from the original Goal thread, collaboration subagents, and a new same-directory child thread, which ruled out one stale unified-exec session registry as the primary cause.

Live /proc evidence before mitigation

The app-server process was captured at 1012 open FDs with a soft limit of 1024 (FDSize=1024, highest occupied FD 1023):

| FD class | Count |
|---|---:|
| pipe | 662 |
| pidfd | 221 |
| regular/session/state files | 111 |
| socket | 6 |
| other anon-inode FDs | 12 |

Of the 111 file FDs, 82 pointed into the Codex session store.

The same app-server had 221 direct children. 219 were live, sleeping docker clients:

| MCP stdio child | Count |
|---|---:|
| mcp-suite-stdio lsp | 74 |
| mcp-suite-stdio codegraph | 73 |
| mcp-suite-stdio agbrowse | 72 |

The Docker MCP clients alone accounted for 657 broker-side pipe FDs + 219 pidfds = 876 FDs. This is approximately 73 retained copies of the three-server MCP set, or roughly 12 broker FDs per agent/context.

An important Linux-specific detail: these were not zombie/dead children. Every one of the 219 pidfds still targeted a live Docker process, and all 657 associated pipe inodes still had a live peer. There were no PTY FDs in the service cgroup. This snapshot therefore looks like retention of live per-agent stdio transports, rather than only failure to reap already-dead children.

Host-wide exhaustion was ruled out:

/proc/sys/fs/file-nr: 29211  0  9223372036854775807
cgroup pids.current / pids.max: 2162 / 18153

Controlled mitigation and toggle evidence

I did not kill/restart the app-server or any MCP process. I only raised the running app-server's soft limit, leaving the hard limit unchanged:

prlimit --pid <app-server-pid> --nofile=8192:524288

After that change, two non-PTY and two PTY executions of /bin/bash -c true, launched concurrently through unified exec, all exited 0. The retained MCP processes and FDs remained; only the headroom changed. A later snapshot still showed 216 Docker MCP children and 994 app-server FDs, confirming that this was a recovery workaround rather than cleanup.

The service definition still has LimitNOFILESoft=1024, so the next app-server restart would inherit the low limit again unless the unit is hardened.

Why this appears to be the same upstream issue

The direct parent of every Docker MCP client was the Codex app-server. The Docker command came from the normal global [mcp_servers.*] config; it was not launched by a still-running LazyCodex wrapper process. The issue therefore reproduces as app-server MCP lifecycle behavior with an ordinary stdio MCP command.

As a local workaround, I can replace these stdio entries with the already-running loopback Streamable HTTP endpoints. That should remove the docker child / pipe / pidfd multiplier, but it does not address per-thread/agent MCP client ownership, sharing, or idle teardown in Codex itself.

This Linux data point suggests a useful regression test: after completing, interrupting, or archiving a large collaboration-agent tree, app-server child-process and FD counts should return close to baseline. It would also be valuable to cover a low inherited RLIMIT_NOFILE, because the lifecycle retention otherwise surfaces as total loss of shell and PTY execution.

Related: #26984.

---
This issue or PR was generated by LazyCodex.
Tag: lazycodex-generated

Lenvanderhof · 7 days ago

Corroborating from a different surface: this also happens with codex-cli on Linux, so it is not Desktop- or macOS-specific.

Environment

  • codex-cli 0.144.3 (terminal CLI, --dangerously-bypass-approvals-and-sandbox)
  • Debian Linux x86_64, 32 GB RAM
  • stdio MCP servers from ~/.codex/config.toml: ferrex (Rust memory server, loads ~2 GB of embedding models at initialize) and a python RAG server via uv run

Observation (2026-07-12/13)

One long-lived codex CLI process (PID 3510112, ~5.5 h old) had accumulated 4 concurrent instances of the same ferrex stdio server and 4 instances of the python server, all still direct children, none ever reaped:

    PID    PPID     ELAPSED  VmSwap  CMD
3510714 3510112    05:22:28  1997MB  ferrex
3934474 3510112    03:36:37  1997MB  ferrex
3935803 3510112    03:36:29  1997MB  ferrex
4064556 3510112    03:05:07  1997MB  ferrex
(+4 matching uv/python MCP children with the same spawn times)

Spawn timestamps cluster in bursts (03:27, 05:13, 05:14, 05:44 local), consistent with a fresh MCP set per thread/turn burst while earlier sets stay alive but idle. With ~2 GB of model weights per instance this pushed the 32 GB machine deep into swap (observed 61 GB swap used across sessions exhibiting this pattern).

Repro sensitivity note

Our MCP server initialized slowly under memory pressure (30–90 s model load). If the respawn path triggers on initialize/tool-call timeouts, slow-initializing stdio servers make the leak far more likely — that may explain why some setups see it constantly and others rarely.

rwang23 · 4 days ago

I can reproduce this on the current Codex Desktop build for Windows. Since #19753 was merged in April, this looks like a lifecycle path that is still missing in Desktop, or a regression after that fix.

Environment:

  • Windows 11 Home, x64, build 26200
  • Codex Desktop package 26.707.12708.0
  • 32 GB RAM
  • Several long-running local tasks across multiple repositories
  • A mix of global, project-scoped, and plugin-provided stdio MCP servers

I recorded the app-server PID, each direct MCP root PID, process creation time, working directory, and private memory before and after a full Codex restart. I did not terminate any process during the capture.

The full restart did clean up the old app-server tree. The app-server PID changed, and all 57 previously tracked direct-child process instances were gone. The problem returned quickly under the new app-server:

  • About 8 minutes after restart, 7 matching runtime bundles were live.
  • About 15 minutes after restart, 12 bundles were live.
  • During one 10-second readback window, the bundle count increased from 10 to 12 while the older bundles remained alive.

At the end of that window, the new app-server owned:

| Process group | Root count | Aggregate private memory |
| --- | ---: | ---: |
| CodeGraph MCP | 12 | 970.0 MB |
| Project-scoped MCP | 2 | 225.5 MB |
| Data Analytics Widgets | 12 | 301.0 MB |
| OpenAI API key local confirmation | 12 | 263.6 MB |
| Sites Design Picker | 0 | 0 MB |
| Built-in node_repl | 12 | 32.2 MB |

The managed stdio and built-in tool trees used about 1.79 GB of private memory. The app-server itself used about 2.16 GB at that point.

There is also a configuration inconsistency that amplifies the leak. These three plugin MCP servers were all set to enabled = false before the new processes were created:

  • sites-design-picker: the setting was honored, with zero processes after restart.
  • dataAnalyticsWidgets: the setting was not honored, with 12 processes.
  • openai-api-key-local-confirmation: the setting was not honored, with 12 processes.

The two project-scoped MCP roots matched two active tasks in that repository, so project scoping itself appears to work. The duplicate global and plugin bundles are the part that keeps growing.

One CodeGraph --liftoff-only child also remained alive across the full app restart after its launcher parent had exited. That may be specific to the third-party launcher, but a Windows Job Object around the task-owned process tree would prevent detached descendants from surviving the owning runtime.

What I expected:

  • A task that becomes notLoaded, is archived, or is evicted should release its stdio transports and terminate the complete process tree after a bounded grace period.
  • Plugin MCP entries with enabled = false should not be started for newly loaded task runtimes.
  • The number of runtime bundles should stay bounded instead of increasing with long-running multi-task use.

I have a sanitized PID and lifecycle matrix and can provide it without local paths, project names, session content, or credentials.

rwang23 · 4 days ago

Follow-up from the same Windows host after extending the read-only process snapshot.

The environment is unchanged: Windows 11 x64, 32 GB RAM, Codex Desktop package 26.707.12708.0, and several long-running tasks. The newer classifier separates the one top-level Desktop app-server from the codex.exe bridge processes launched under built-in Node REPL hosts.

At 2026-07-16T21:57:38Z, the top-level app-server had 104 descendants and 56 managed service roots:

| Process group | Root count | Process count | Private memory |
| --- | ---: | ---: | ---: |
| CodeGraph | 14 | 43 | 1,494.1 MB |
| Built-in Node REPL | 14 | 20 | 2,440.8 MB |
| Plugin MCP CJS entrypoint | 14 | 14 | 352.9 MB |
| Plugin MCP MJS entrypoint | 14 | 14 | 305.0 MB |

Those service trees used about 4.59 GB of private memory. The app-server itself used another 1.24 GB.

The Node REPL number needs some unpacking. There were 14 lightweight node_repl.exe hosts, but only three had an active Node kernel and Codex bridge. One persistent kernel alone was using 2,217 MB of private memory and had accumulated 2,960 CPU seconds. The same kernel had dropped to about 408 MB in an earlier sample, then grown again. This looks like a persistent per-task execution state with large allocation and GC swings, not 14 equally heavy REPL processes.

I also saw partial cleanup on this build. The four root groups fell from 19 each at 21:39Z to 14 each at 21:57Z without any manual process termination. So "never cleaned up" is too strong for this Windows host. The behavior is still delayed or incomplete: all four groups move together, and every retained task runtime keeps the full bundle even when most Node REPL hosts have no active kernel.

A separate 30-second multi-session sample recorded 237.7 MB of app-server reads, 36.5 MB of writes, 41,433 read operations, 5,703 write operations, and 5.36 CPU seconds. These are Windows process counters and include files, pipes, and network traffic, so I am not treating them as pure disk I/O.

I found one third-party multiplier as well. CodeGraph 1.4.1 relaunches its Node adapter with V8's --liftoff-only flag when that flag is missing from the original Node command line. Supplying the flag directly should remove one wrapper process per CodeGraph root after the next Desktop restart. That reduces the cost of this particular MCP, but it does not explain why Codex retains one complete adapter bundle per loaded task.

The upstream behavior would be much easier to diagnose and contain if Desktop did the following:

  • Expose a stable task or runtime owner ID in process diagnostics.
  • Close the stdio transport and the complete Windows process tree when that owner is archived, evicted, or unloaded.
  • Start global and plugin MCP servers on first use, then apply an idle eviction policy.
  • Honor enabled = false before constructing a new task runtime.

I can provide another sanitized PID and start-time matrix if a maintainer needs it. No project paths, session content, command lines, daemon tokens, or credentials were captured in the snapshot output.

sleepfrontofmtv · 3 days ago

I reproduced the same issue on a newer Codex Desktop build, at substantially larger scale. There was also an unreaped-zombie component similar to #12491.

Environment

  • Codex Desktop: 26.715.21425
  • Bundled CLI: codex-cli 0.145.0-alpha.18
  • macOS 26.5.2 (25F84), Apple Silicon (arm64)
  • 128 GB RAM, 20 logical CPUs
  • Relevant global stdio MCP definitions:
  • npx -y chrome-devtools-mcp@latest
  • uvx --from git+https://github.com/oraios/serena serena start-mcp-server --project-from-cwd --context codex
  • headroom mcp serve

Observed failure

After a long-running desktop session with many threads/background agents, the host became nearly unusable. A process snapshot showed:

  • Load average around 191 / 512 / 636 at capture time (earlier samples were approximately 520-686)
  • 373 live chrome-devtools-mcp processes, consuming about 898% aggregate CPU
  • 362 uv launcher processes plus 362 Python Serena server processes (about 724 Serena-related process entries)
  • 386 headroom mcp serve processes
  • 1,291 Node processes in total
  • About 2,877 zombie children below the bundled Codex app-server process (2,884 zombies system-wide)

Nearly every sampled Chrome DevTools MCP parent chain led back to the same bundled Codex app-server process. The affected app-server had been running for roughly seven hours.

This was not primarily RAM exhaustion. The machine still had substantial free memory; the freeze was caused by run-queue/process-table pressure from thousands of live and zombie processes.

Recovery result

After restarting the Codex/ChatGPT application stack, counts returned to:

  • chrome-devtools-mcp: 1
  • Serena server: 1
  • Headroom server: 1
  • Aggregate CPU for those three: 0% at the verification sample
  • System memory free: 94%

The three remaining system zombies were traced to LM Studio and two SSH sessions, not Codex/MCP.

I do not yet have a deterministic minimal reproduction beyond sustained multi-thread/background-agent use with multiple global stdio MCP servers. However, the parent-process evidence and the immediate return to one instance per MCP after restarting Codex strongly support the per-thread lifecycle leak described here.

In addition to terminating MCP children when their owning thread ends, the app-server should reap exited children and ideally provide a shared/singleton MCP mode or a hard per-server instance limit. A process-count or zombie-growth guard would also prevent the desktop from taking down the host while the underlying lifecycle bug is being addressed.

andheller · 3 days ago

Additional reproduction on the current unified ChatGPT/Codex macOS app, with a strong per-tool-execution lifecycle correlation.

Environment

  • ChatGPT/Codex app build: 26.715.21425
  • Codex Framework: 150.0.7871.124
  • Bundled Codex CLI: 0.145.0-alpha.18
  • macOS: 26.5.1 (25F80)
  • Apple M5, 16 GB RAM

Reproduction and lifecycle evidence

After a fresh reboot/app launch, there were 5 copies of each configured local stdio MCP root. During four read-only diagnostic/tool-execution cells in a single existing task, the npm exec xcodebuildmcp@latest mcp root count rose from 5 to 9. The elapsed start times of the four new MCP sets aligned with the four tool cells. No new user task was opened during that sequence.

Each retained set included local servers/helpers such as:

  • xcodebuildmcp
  • a local SEO MCP
  • a local site/CLI MCP
  • plugin-provided Node MCP servers
  • Computer Use event stream
  • Node REPL

This appears to be more granular than only “one set per visible conversation”: completed tool/code-mode execution cells can initialize another full MCP set that remains parented by the persistent codex app-server.

Impact observed before reboot

After extended use:

  • 954 total processes / 8,032 threads
  • 381 direct children under one codex app-server
  • 297 node processes
  • about 41–42 copies of several MCP roots
  • load averages around 28–36
  • system CPU around 64%
  • 23.8–24.4 GB swap used on a 16 GB Mac
  • roughly 740–790 MB/s disk reads during the paging storm
  • no thermal warning and more than 120 GB free storage

The app-server itself sampled around 83–148% CPU.

Terminating only the duplicated MCP trees removed 472 processes:

  • total processes fell from about 951 to 465
  • node processes fell from 307 to 17
  • swap began unwinding immediately, falling from about 24.4 GB to 13.3 GB within seconds

Relative cost seen locally

Among three specialized globally configured servers, summed RSS per retained tree was approximately:

  • xcodebuildmcp: 90–300 MB
  • SEO MCP: about 110 MB across four processes
  • local site/CLI MCP: about 36 MB

RSS includes shared pages, so these figures are directional rather than perfectly additive.

Workaround

Moving specialized stdio MCPs out of global config and into trusted project-level .codex/config.toml files substantially reduces the fan-out, but it does not address teardown of already-started sets. Fully quitting the app remains the reliable way to reap the persistent app-server tree.

No raw logs or session files are attached because they may contain private prompts and project metadata.

aidawilliam41-ops · 3 days ago

Additional Windows reproduction on a newer Codex Desktop build, showing partial cleanup but rapid recreation of complete MCP process pools.

Environment:

  • Codex Desktop 26.715.2305.0
  • Windows 11 Pro 25H2, build 26200.8037, x64
  • Main app-server: codex.exe, parent ChatGPT.exe

Observed:

  • 755 total processes at capture time, including 258 node.exe and 239 cmd.exe.
  • The main app-server tree reached 522 descendants, 3,921 threads, 106,740 handles, and 17.69 GB private memory.
  • A typical complete runtime pool contained eight MCP servers, node_repl, two plugin server.mjs processes, and their cmd/npx/node children: 31 processes and approximately 1.07–1.24 GB private memory.
  • Some pools remained alive for more than five hours.
  • At least 19 matching GitHub-MCP/node_repl pools were alive, while native list_threads returned only 10 recent tasks: 3 active, 2 idle, and 5 notLoaded.
  • Cleanup was not strictly absent: over 66.7 seconds, the classified pool count changed from 14 to 13 while 45 PIDs appeared and 107 disappeared. However, a new complete pool was created during that interval.
  • Additional agent/tool executions correlated with new complete pools at 17:14:07, 17:14:16, 17:14:25, 17:14:32, and 17:14:37.

Performance impact:

  • During a clean 10.76-second sample, the app-server tree consumed 9.81% of total capacity across 16 logical CPUs.
  • System context switches averaged 175,583/s and peaked at 214,986/s; processor queue peaked at 7.
  • DPC, interrupts, disk queue, and available RAM did not indicate hardware, storage, or memory exhaustion.
  • The user observed visible Desktop freezes and mouse stutter.

Expected:
MCP runtimes should be reused or terminated after their owning task/tool execution becomes idle, unloaded, superseded, or complete. Pool count should remain bounded by actually loaded work.

Workaround:
A full Codex Desktop restart clears the accumulated tree temporarily. No permanent safe workaround is confirmed.

iddictive · 2 days ago

Independent reproduction on the current macOS Desktop build, with a concrete pooling proposal.

Environment

  • Codex Desktop 26.715.21425 (build 5488)
  • Bundled CLI 0.145.0-alpha.18
  • Darwin 24.6.0 arm64
  • 48 GiB RAM

Observation

The long-lived Desktop app-server had been running for about 24 hours. During a read-only diagnostic window, the number of Node processes attributable to configured/plugin MCP servers increased from 890 to 913, then to 945, without restarting the app-server.

At the final snapshot:

  • 945 MCP-related Node processes
  • about 19.2 GiB aggregate MCP RSS (RSS includes shared mappings, so this is not unique physical memory)
  • 789 Node processes were direct children of the Desktop app-server
  • those direct children accounted for about 16.5 GiB aggregate RSS
  • the app-server itself was using about 1.8 GiB RSS

The process tree consisted of repeated, session-shaped batches of the same enabled stdio MCP commands and package-runner wrappers. The affected MCPs were mostly idle at capture time. A full Desktop restart is currently the only reliable way to return the process tree to baseline, but that interrupts every active task.

Suggested manager contract

The app-server should own a singleton MCP broker instead of attaching an eagerly spawned stdio stack to every loaded thread or subagent:

  1. Pool shareable upstreams by a canonical configuration fingerprint: transport, command/URL, arguments, cwd, plugin version, and names of forwarded environment variables. Secret values must not appear in diagnostics or pool keys.
  2. Give threads and subagents lightweight leases on pooled upstreams. Release leases on completion, interruption, archive, unsubscribe, or eviction.
  3. Start stdio upstreams lazily on first tool use and stop them after a bounded idle TTL when the reference count reaches zero.
  4. Add an explicit scope for servers that cannot be shared safely: global, project, or session. Stateful browser/computer-control servers should not be silently pooled across unrelated tasks.
  5. Connect native Streamable HTTP upstreams directly instead of creating a local stdio bridge per session.
  6. Enforce a global app-server budget for MCP process count, RSS, file descriptors, and startup rate. Surface the owning server, scope, lease count, last tool call, and idle deadline in Desktop diagnostics.
  7. Add a “Restart MCP runtime” action that restarts only the broker/upstreams, without terminating active Codex tasks.

The important separation is logical thread residency versus MCP runtime residency. Keeping a completed thread available for resume should not require keeping its entire stdio process stack alive.

All project names, local paths, credentials, account identifiers, and command arguments have been omitted. I can provide sanitized aggregation commands if useful.

kkkzbh · 1 day ago

Correction and narrowed scope for this Fedora datapoint.

This reproduction used the official managed standalone Codex CLI 0.144.6 in app-server mode, driven by a custom Linux app-server client. It should not be read as an official Linux client/support report.

I checked the client boundary:

  • the client does send thread/unsubscribe
  • app-server returns status: unsubscribed
  • the local Linux integration does not patch the thread/unsubscribe / thread/closed lifecycle

I also ran an isolated 0.144.6 app-server probe with a disposable, well-behaved stdio MCP server:

  • thread/unsubscribe left the MCP alive, which matches the documented 30-minute unloaded-thread residency
  • thread/archive transitioned the thread to notLoaded and the corresponding MCP process exited
  • stopping app-server also removed the remaining probe process

Therefore the original 39-child snapshot is not sufficient to claim that every child was leaked by app-server. It conflated three possible contributors: expected 30-minute thread residency, missing EOF/signal shutdown in some custom MCP servers, and any genuine CLI-side runtime retention. I retract the stronger causal conclusion from the earlier wording.

The snapshot still establishes that the official CLI process owns the child roots and pipe endpoints. The independent long-running TUI reproduction in #26984 remains the stronger evidence that the broader MCP/pipe lifecycle problem can occur without any app-server GUI client. A conclusive 0.144.6 app-server report should correlate a specific MCP PID with thread/closed or an explicit archive/unload boundary and show that it survives that boundary.