MCP server processes leak: per-thread processes never cleaned up (9+ GB RSS)
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
- Configure global MCP servers in
~/.codex/config.toml(e.g.playwright,davinci-resolve,node_repl) - Open Codex Desktop, create ~25 threads over a session
- Close/archive threads as normal
- 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
sharedorreusemode so all threads share a single MCP process instance - Consider an
idle_timeoutoption to auto-kill MCP processes after N minutes of no use
32 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
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-serverprocess 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(viatsx) ×5, plus the uv-based servers (arxiv-mcp-server,semantic-scholar-mcp,reddit-*) at similar multiplicity. System-wide that was 128node+ 24uv** 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:
npm execdoubling — each stdio server launched vianpm exec <pkg>is two processes (the npm wrapper + the actual node), so N configured servers × M threads produces ~2·N·M processes.The cost isn't only RAM — it's disk exhaustion and system-wide failure, which I think deserves emphasis alongside the RSS number:
vm.swapusagewas at ~2.5 GB used with multiple/System/Volumes/VM/swapfile*present.lsof +L1shows unlinked-but-open inodes pinned by the stalenodeprocesses, so that space is never reclaimed by the OS.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:
shared/reusemode so all threads share one MCP instance (biggest win — collapses the ×M entirely), and/oridle_timeoutto auto-kill idle MCP processes.Happy to provide sanitized
ps/lsofcaptures through a private channel if useful.I can reproduce a similar leak on macOS with the current Codex Desktop app.
Environment:
26.623.101652467426.5.1(25F80)arm64/Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabledObserved on July 6, 2026 JST under the live app-server PID:
The
node_repland 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:
Actual behavior:
node_repland 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-memorydaemon 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_repland MCP proxies).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:
ulimit -n:256launchctl limit maxfiles: observed as65536 unlimited, but Codex-spawned shell still inheritedulimit -n = 256Observed app-server processes:
There was also an orphaned launcher shell:
The GUI app-server had duplicated MCP children:
The Unix-socket app-server also had duplicated MCP children:
The generic
./mcp/server.*processes mapped back by cwd to plugin-bundled MCPs:This eventually reproduced inside Codex as:
So this is not only memory/process accumulation; with a low inherited
RLIMIT_NOFILE, the app-server can become unable to spawn new commands.Independent severe reproduction on macOS, adding Jetsam-level evidence to this MCP lifecycle leak. The system became unresponsive and required a manual restart.
Environment
26.707.31123(bundle build5042)codex-cli 0.144.0-alpha.426.5.1(25F80)macOS Jetsam evidence
macOS wrote two
JetsamEventreports 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
nodeprocesses |nodein 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:
nodecount increased by 83At 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
nodediagnostic report identifies the ownership chain as: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:
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.
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
codex app-server --listen unix://systemd --userserviceRLIMIT_NOFILE: soft1024, hard524288docker 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:
The PTY path failed independently:
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
/procevidence before mitigationThe app-server process was captured at 1012 open FDs with a soft limit of 1024 (
FDSize=1024, highest occupied FD1023):| 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
dockerclients:| 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:
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:
After that change, two non-PTY and two PTY executions of
/bin/bash -c true, launched concurrently through unified exec, all exited0. 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
dockerchild / 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
Corroborating from a different surface: this also happens with codex-cli on Linux, so it is not Desktop- or macOS-specific.
Environment
--dangerously-bypass-approvals-and-sandbox)~/.codex/config.toml:ferrex(Rust memory server, loads ~2 GB of embedding models at initialize) and a python RAG server viauv runObservation (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:
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.
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:
26.707.12708.0I 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:
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 = falsebefore 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-onlychild 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:
notLoaded, is archived, or is evicted should release its stdio transports and terminate the complete process tree after a bounded grace period.enabled = falseshould not be started for newly loaded task runtimes.I have a sanitized PID and lifecycle matrix and can provide it without local paths, project names, session content, or credentials.
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 thecodex.exebridge 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.exehosts, 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:39Zto 14 each at21:57Zwithout 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-onlyflag 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:
enabled = falsebefore 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.
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
26.715.21425codex-cli 0.145.0-alpha.1826.5.2(25F84), Apple Silicon (arm64)npx -y chrome-devtools-mcp@latestuvx --from git+https://github.com/oraios/serena serena start-mcp-server --project-from-cwd --context codexheadroom mcp serveObserved failure
After a long-running desktop session with many threads/background agents, the host became nearly unusable. A process snapshot showed:
191 / 512 / 636at capture time (earlier samples were approximately520-686)373livechrome-devtools-mcpprocesses, consuming about898%aggregate CPU362uvlauncher processes plus362Python Serena server processes (about724Serena-related process entries)386headroom mcp serveprocesses1,291Node processes in total2,877zombie children below the bundled Codex app-server process (2,884zombies 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:1110%at the verification sample94%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.
Additional reproduction on the current unified ChatGPT/Codex macOS app, with a strong per-tool-execution lifecycle correlation.
Environment
26.715.21425150.0.7871.1240.145.0-alpha.1826.5.1 (25F80)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 mcproot 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:
xcodebuildmcpThis 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:
codex app-servernodeprocessesThe app-server itself sampled around 83–148% CPU.
Terminating only the duplicated MCP trees removed 472 processes:
nodeprocesses fell from 307 to 17Relative cost seen locally
Among three specialized globally configured servers, summed RSS per retained tree was approximately:
xcodebuildmcp: 90–300 MBRSS 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.tomlfiles 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.
Additional Windows reproduction on a newer Codex Desktop build, showing partial cleanup but rapid recreation of complete MCP process pools.
Environment:
Observed:
Performance impact:
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.
Independent reproduction on the current macOS Desktop build, with a concrete pooling proposal.
Environment
26.715.21425(build5488)0.145.0-alpha.18Darwin 24.6.0 arm64Observation
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
890to913, then to945, without restarting the app-server.At the final snapshot:
945MCP-related Node processes19.2 GiBaggregate MCP RSS (RSS includes shared mappings, so this is not unique physical memory)789Node processes were direct children of the Desktop app-server16.5 GiBaggregate RSS1.8 GiBRSSThe 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:
global,project, orsession. Stateful browser/computer-control servers should not be silently pooled across unrelated 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.
Correction and narrowed scope for this Fedora datapoint.
This reproduction used the official managed standalone Codex CLI
0.144.6inapp-servermode, 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:
thread/unsubscribestatus: unsubscribedthread/unsubscribe/thread/closedlifecycleI also ran an isolated
0.144.6app-server probe with a disposable, well-behaved stdio MCP server:thread/unsubscribeleft the MCP alive, which matches the documented 30-minute unloaded-thread residencythread/archivetransitioned the thread tonotLoadedand the corresponding MCP process exitedTherefore 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.6app-server report should correlate a specific MCP PID withthread/closedor an explicit archive/unload boundary and show that it survives that boundary.Independent reproduction on a newer macOS Codex Desktop build.
Environment
26.715.72028(build5706)150.0.7871.12426.4(build25E246)Observed process retention
A process snapshot reported:
cua_node ... kernel.jsprocesses withPPID=1The persistent Desktop app-server had repeated direct-child groups with this shape:
The oldest retained group had been alive for about 1 day 17h 55m. Several other groups were older than one day, two hours, or tens of minutes while sleeping at 0% CPU.
During the controlled investigation, sequential tool calls created three additional complete
node_repl + server.cjs + server.mjsgroups within about five minutes. Completed calls did not remove the earlier groups; every group remained a direct child of the same long-lived app-server.Controlled cleanup result
I sent
SIGTERMonly to:PPID=1orphan kernels, andThe active Desktop/app-server processes, current tool helpers, and unrelated application processes were explicitly preserved.
Result: 48 stale helper processes exited successfully, while Codex Desktop and the active task continued running. This confirms that these were retained helper trees rather than required Chromium virtual-memory mappings.
Performance correlation
At capture time:
~/.codex/logs_2.sqlitewas about 348 MiB,There was no pageout or swapout increase during the capture. The apparent 1.87 TiB VSZ was verified as sparse Chromium/V8 address space (physical footprint about 519 MiB), so it is not the cause being reported here.
Expected behavior
Tool-scoped MCP/Node REPL processes should be terminated when a tool call or owning task runtime completes, or reused through a bounded pool with idle eviction. Detached
PPID=1kernels should also be reaped.Additional evidence
A timestamped local diagnostic bundle contains the process tree, elapsed times, memory pressure, SQLite sizes, disk I/O samples, and a main-process stack sample. I have not attached the raw bundle because it includes local project paths and command arguments, but I can provide a sanitized version if maintainers need it.
Reproduced on Codex 0.145.0 with Neon MCP 0.6.5. Codex spawned 13+ local workers despite remote OAuth configuration; one reached ~4.2 GiB RSS and 438% CPU in under a minute. Eight workers later totaled ~26.3 GiB, causing system-wide input/audio failure and a reboot. Related: #18881, #17832.
I found a distinct trigger for this multiplication: Codex’s hidden internal tasks.
During one visible task, Codex started four Serena processes for the same project within 23 seconds; each launched its own
rust-analyzer. One startup was directly tied to the visible task. Two others coincided with app-server’sambient_suggestionsandthread_titletasks, and the fourth with an unlabeledthread/start. The latter mappings are timestamp-based because Serena logs lack Codex thread IDs.Existing reports cover user-created threads, subagents, Goals, and process retention. This suggests non-user-visible internal tasks also inherit and eagerly initialize the user’s MCP configuration.
Follow-up: displaying a completed automation thread reproducibly starts a thread-correlated MCP/helper process bundle
This is a current-build follow-up to #30408, not a claim that every part of the original report still behaves the same way.
Executive summary
On the tested macOS Codex App build and helper configuration:
+10in each of two reversible A-B-A replications;-10, back to the prior baseline;The narrow conclusion is that navigating to/displaying the tested completed automation threads was the observed eager-start trigger for a thread-correlated MCP/helper process bundle in this build. The experiment does not distinguish rendering from other task-activation work performed by the app, does not establish the internal code-level cause, and does not show that every thread type, platform, or MCP configuration behaves this way.
For quick triage, the shortest path through this comment is:
The investigation chronology, reporting decision, template-alignment notes, and attachment decision are included later in collapsible sections.
Actual behavior in this build
Displaying the history of a completed local scheduled-automation thread started a repeated set of local MCP/helper processes even though no MCP tool was invoked. The added processes remained present across the three short samples collected during each displayed phase. Explicitly rearchiving the tested thread and returning away from it restored the matching process count to the previous baseline.
This differs from the exact cleanup behavior described in the original #30408 report: the combined rearchive-and-return step restored baseline in this reproduction, while display/load still eagerly started a thread-correlated helper set. Because rearchiving and returning away were performed together, this experiment does not establish which part of that combined step performed the cleanup. It is also not proof that archive cleanup is fixed globally.
Test environment
26.721.81911(CFBundleVersion 5973)codex-cli 0.146.0-alpha.3.1uname -mprsoutput:Darwin 27.0.0 arm64 arm27.0(26A5378n)v24.15.0xcodebuildmcpnode_repluv-launched helpersSubscription plan, session/thread IDs, token-limit usage, and context-window usage are intentionally omitted; the reasons are documented in the collapsed “Template alignment and intentional deviations” section below.
Minimal reproduction
A0).B1). Rearchive it, return away, and collect three samples (A1).B2). Rearchive it, return away, and collect three samples (A2).The primary metric was a system-wide count of unique PIDs matching the same OR-expression in every phase:
An equivalent read-only counter is:
The character-class spelling, such as
[n]ode_repl, prevents the counter command from matching its own command line. Exact process names will vary with the configured helpers.Measurement design
Before changing thread state for the controlled crossover, I designated:
node_repl,npm, Computer Use, anduv.The experiment used:
No process was manually signaled or killed. No application, container, build, download, or VM was stopped. All three test threads were returned to their original archived state.
Primary results
Two-thread reversible crossover
| Phase | Operation | MCP/helper process count |
| --- | --- | --- |
| A0 | Rearchived baseline |
131, 131, 131|| B1 | Unarchive and display completed thread 1 |
141, 141, 141|| A1 | Rearchive thread 1 and return |
131, 131, 131|| B2 | Unarchive and display completed thread 2 |
141, 141, 141|| A2 | Rearchive thread 2 and return |
131, 131, 131|Observed effect for this matcher and these two threads:
Trigger separation with a third thread
| Operation | MCP/helper count | Direct-child tuple |
| --- | --- | --- |
| Rearchived baseline |
131, 131, 131|14/53/14/13|| Unarchive only; do not display |
131, 131, 131|14/53/14/13|| Navigate to/display the thread |
141, 141, 141|15/57/15/14|| Rearchive and return |
131, 131, 131|14/53/14/13|Direct-child tuple order:
This identifies navigating to/displaying the task as the observed trigger in this reproduction. It does not distinguish the visual rendering action from broader task activation, restoration, or app-server initialization performed at the same time.
Direct-child mechanism evidence
In the clean second crossover replicate (
A1 -> B2 -> A2), direct children of the live Codex app-server changed reversibly:Under this configuration, displaying one tested completed thread coincided with:
npmchildren;node_repl;uv; andBecause the matcher includes Computer Use,
node_repl, anduv-launched helpers, this report uses “MCP/helper process bundle” rather than describing all ten processes as MCP servers.Expected behavior
Merely displaying the history of a completed or idle automation thread should not eagerly start a full configured MCP/helper set when no MCP tool is invoked during the display step.
If eager initialization is required by design, I would expect the ownership and cleanup behavior to be bounded and observable—for example, cleanup after leaving the thread or after an idle/completion period.
Resource impact
In the clean second crossover replicate, summed RSS for the matching MCP/helper processes changed as follows:
| Phase | Summed matcher RSS |
| --- | ---: |
| Rearchived baseline | approximately
1,852 MiB|| Thread displayed | approximately
2,449 MiB|| Thread rearchived | approximately
1,903 MiB|The displayed phase was therefore approximately
546–597 MiB(0.53–0.58 GiB) above the adjacent archived measurements in that replicate. This is not presented as an exact per-thread physical-memory cost:+10/-10process-count result is the stronger finding.Total-process medians moved in the same direction but drifted in the background:
Those totals support direction only; they do not establish an exact total-process cost per displayed thread.
Swap moved from approximately
7,113.94 MiBbefore the first load to7,244.81 MiBafter it and ended at7,228.81 MiB. Because swap is hysteretic and did not promptly reverse, it is excluded from the causal claim.Evidence-supported conclusions and limits
| Classification | Statement |
| --- | --- |
| Directly observed | Unarchive-only produced no matcher-count change for the third tested thread. |
| Directly observed | Displaying each of two tested completed threads produced the same
+10matcher count in all three short samples. || Directly observed | Rearchiving each tested thread and returning away restored the prior matcher count. |
| Directly observed | The app-server direct-child tuple changed and reversed with the displayed/rearchived state. |
| Supported inference | The added processes form a thread-correlated MCP/helper set under this configuration. |
| Not established | Whether rendering, task activation, state restoration, or another simultaneous app-server action is the internal trigger. |
| Not established | Whether all Codex thread types, platforms, app versions, or helper configurations behave the same way. |
| Not established | An exact unique physical-memory cost per thread. |
| Not established | That the earlier system-wide swap, load-average, or disk-space improvement was caused solely by archiving. |
| Not established | That the original archive-cleanup problem in #30408 is fixed globally. |
Current safe workaround
For the three tested automation threads on this build, explicitly archiving a completed thread and returning away from it restored the matcher count to baseline. This is a reversible workaround, not a general fix or a guarantee for other thread types/configurations.
Possible engineering directions
These are non-prescriptive possibilities, not claims about the current implementation:
The automation-history UX is related but distinct from the process-lifecycle bug; see #30515. Eager per-session MCP startup is also tracked in #21984.
Sources and evidence provenance
Sources were verified on
2026-07-30JST. Official repository documents are linked to fixed commits to avoid latermain-branch drift. No secondary articles or third-party documentation were used.Official OpenAI repository guidance
d53e68954acee2eb50303970498ffebddff393ed12f69b893f10393ef48fa8a3ac460a8b6f4438f7Prior primary reports in the official repository
These are primary reports from repository users, not authoritative product specifications:
Direct local evidence
All numerical results in this comment are direct observations from the tested Mac on
2026-07-30JST. They were collected with short, bounded, read-only process/system commands, except for the explicitly authorized reversible thread archive/unarchive/display operations used in the crossover. No prompt contents, user paths, account identifiers, or session/thread IDs are included.<details>
<summary><strong>Investigation chronology and decision trail</strong></summary>
1. Initial health check
A bounded Mac health check found both:
At the degraded observation point:
1,156;349;1,804 MiB;17,011.44 MiBof18,432 MiB; and32%free.The File Provider problem was kept separate from the Codex/MCP investigation because it remained active after the Codex-thread action.
2. First reversible remediation
Twenty-five completed, inactive runs of the same monitoring automation were archived. Active work and unrelated active tasks were preserved.
Immediately afterward:
| Measurement | Before | After |
| --- | ---: | ---: |
| Total processes |
1,156|760|| MCP/helper matcher count |
349|119|| Summed matcher RSS | approximately
1,804 MiB| approximately929 MiB|| Swap used | approximately
17,011.44 MiB| approximately7,193.94 MiB|| Memory-pressure free percentage |
32%|45%|| Load average |
6.82 / 6.59 / 6.85|4.10 / 3.55 / 4.46|These were observational before/after measurements. They motivated the controlled test but were not treated as sufficient proof that archiving alone caused every system-wide improvement.
In particular:
92–94%CPU;3. Decision to require a controlled test
The initial process-count reduction was large and consistent with completed threads releasing repeated helper sets, but spontaneous idle cleanup and macOS memory/swap housekeeping could not be excluded.
A strict unarchive/display/rearchive test was initially deferred because it would deliberately reintroduce load. It was performed only after explicit permission for that reversible test.
4. Controlled crossover
Two completed threads were tested with the same primary outcome and three samples per phase:
The exact
131 -> 141 -> 131 -> 141 -> 131pattern was reproduced, with each phase stable across all three short samples.5. Trigger-separation control
A third completed thread separated:
Unarchive-only remained at baseline. The
+10change appeared only after navigating to/displaying the thread and reversed after rearchiving.6. Reporting-path decision
The finding was classified as a reproducible Codex App/app-server process-lifecycle bug or behavioral defect, not a setup-help question.
The official contributing guide favors adding analysis and reproduction evidence to the relevant issue thread. #30408 was selected as the primary target because it already covers Codex App MCP process lifecycle and memory impact. #21984 and #30515 are linked as related but distinct issues.
</details>
<details>
<summary><strong>Template alignment and intentional deviations</strong></summary>
This is a detailed comment on an existing issue, not a new issue submission. The current Codex App bug-report template was nevertheless used as the structural baseline.
| Template expectation | Status | Treatment and rationale |
| --- | --- | --- |
| Search for an existing issue | Complied | #30408 was identified as the closest existing Codex App/app-server/MCP lifecycle report. |
| Prefer a reaction over a duplicate comment | Intentional exception | This is not a bare duplicate or “me too” report. It adds materially new current-build evidence: the combined rearchive-and-return step restored baseline, display/load was separated from unarchive-only, and the
+10/-10lifecycle result was replicated. The contributing guide explicitly invites reproduction details and analysis in the issue thread. || Codex App version | Complied with equivalent source | Exact version/build values are included. They were read from the active signed app bundle rather than manually transcribed from the About dialog. |
| Subscription plan | Intentionally omitted | This is an existing-issue comment, and subscription tier is not part of the tested local process-lifecycle mechanism. It can be supplied if a maintainer identifies a diagnostic need. |
| Platform | Complied | Exact
uname -mprsoutput, macOS version/build, architecture, chip, and memory are included. || Actual issue | Complied | An explicit “Actual behavior” section appears near the top. |
| Reproduction steps | Complied | Numbered steps, the fixed matcher definition, an executable read-only counter, repeated phase measurements, and the third-thread control are included. |
| Session/thread ID | Intentionally omitted as not applicable | The result was reproduced across three completed threads and does not depend on one session-specific trace. Publishing identifiers is unnecessary for the initial report. One representative ID can be supplied if a maintainer requests trace correlation. |
| Token-limit and context-window usage | Intentionally omitted as not applicable | No token-limit or context-window boundary was involved in the display-triggered process change. |
| Expected behavior | Complied | Included explicitly and phrased as an expectation rather than an implementation assertion. |
| Additional information | Complied | Resource impact, limitations, workaround, possible directions, related issues, provenance, and safety controls are included. |
| PII redaction and text-first evidence | Complied | The document contains text and tables, not screenshots; prompt contents, private user paths, account identifiers, and thread IDs are excluded. |
</details>
<details>
<summary><strong>Why no CSV or diagnostic archive is attached</strong></summary>
A CSV is not required by the Codex App bug template. For this experiment, the complete primary dataset is only five crossover phases with three identical count samples per phase, plus a four-phase trigger-separation control. The full values are therefore easier to review in the Markdown tables above.
No original timestamped CSV was captured during the experiment. Reconstructing a CSV afterward from summary notes could imply sample-level timestamp precision or provenance that does not exist, so no synthetic CSV is being attached.
If maintainers request a new run, a timestamped machine-readable capture can be collected prospectively with fields such as:
No broad diagnostic archive is attached because it may contain unrelated paths, commands, or session metadata. A narrowly scoped, redacted capture can be supplied if a maintainer identifies the exact fields needed.
</details>
Still present on
0.146.0-alpha.9.2, and the accumulation is linear with app-server uptime, not loadAnother macOS reproduction, with a controlled restart that pins down the mechanism and a same-machine control from a different agent host.
Environment
codex-cli 0.146.0-alpha.9.2(/Applications/ChatGPT.app/Contents/Resources/codex ... app-server) — newer than any build cited above in this thread[mcp_servers.*]blocks in~/.codex/config.toml1. A fresh app-server is correct; it degrades purely as a function of uptime
I tore the previous app-server down cleanly and re-measured the replacement twice:
| app-server uptime | direct MCP children |
|---|---|
| ~10 min | 19 — one clean fleet |
| 82 min | 116 |
| 11 h 16 m (prior instance) | 464 → 541 over a 30-min observation window |
≈1.4 children/min, roughly one new fleet every ~12 minutes, with no correlation to how much I was actually using it. A newly started app-server spawns exactly one fleet and is behaving correctly. This rules out a per-server leak or a runaway individual server, and is consistent with the per-conversation-fleet-never-reaped mechanism in the original report.
2. The uniform count is the tell
At the 11 h mark the prior instance held exactly 24 copies of every configured server — identical count across
xcodebuildmcp,node_repl,n8n-mcp,cua-driverand four others, no outliers. Subtree: 890 total descendants, 32.3 GB RSS.Uniformity across heterogeneous servers (Node, Python, Rust, native) means whole fleets are being duplicated, not that any one server is failing to exit.
3. Same-machine control: a session-owned parent does not accumulate
Measured at the same moment on the same machine, with a comparable MCP config: 11 live Claude Code sessions, each holding a steady 21–22 children, zero accumulation. There the session process is the MCP parent, so a fleet dies with its owner.
The difference is architectural rather than a bug in any individual server: the Codex app-server is a long-lived singleton host, so per-conversation fleets have no owner whose exit terminates them. That also explains why fleet count tracks app-server uptime rather than thread count or activity.
4. Hazard worth flagging for anyone applying a manual workaround
Killing the app-server PID first makes things strictly worse. All ~890 descendants get reparented to
launchd, converting them into genuine orphans and freeing nothing — you lose the one handle that could still reach them. Teardown has to go children-first (deepest-first), then the parent.Second caveat: on this machine live
xcodebuild testruns are also children of the same app-server, so a blind subtree kill will silently take out in-flight builds. Any teardown — manual, or an eventual idle-reaper implementation — needs to check for in-flight work rather than killing the subtree wholesale.5. Impact and recovery
Before teardown the machine was at 418 MB unused / 55 GB compressor / load average 155. After a children-first teardown of 630 descendants, ~70 minutes later: 39 GB unused / 9.7 GB compressor / load 10.7.
So the teardown is an effective mitigation — but it has to be repeated as uptime accrues, which makes it a chore rather than a fix.
Asks (unchanged from the original report, restated with this evidence behind them)
startup_timeout_secandtool_timeout_secare the only lifetime-adjacent knobs available today and neither bounds process lifetime.launchd.Correction to my previous comment: accumulation is faster under load, not purely uptime-linear
A later datapoint on the same machine complicates the "independent of load" framing I used above, so I want to correct it rather than leave it standing.
A replacement app-server, measured during a period of heavy
xcodebuildactivity:| | uptime | direct children | rate | subtree RSS |
|---|---|---|---|---|
| mostly-idle instance | 82 min | 116 | ~1.4/min | — |
| build-heavy instance | 72 min | 557 | ~7.7/min | 31.3 GB |
Roughly 5.5× the idle rate. 57 of those descendants were
xcodebuildprocesses, so part of the gap is build tooling itself — but not all of it, and the fleet count clearly rises faster when the app-server is busy.So the accurate statement is: fleets accumulate with uptime and never reap, and the rate scales with activity. A fresh app-server is still correct (~19 children), and the leak is still that fleets are never released — but "one fleet per ~12 minutes" is a floor, not a constant. Under load the machine reaches a pathological state considerably faster than the idle curve predicts.
Practical note for anyone scripting a workaround
This makes the in-flight-build guard more important, not less. My teardown script refuses whenever any
xcodebuildis in the subtree — and in this measurement that guard blocked a teardown that would otherwise have killed 57 live build processes. The corollary is uncomfortable: on a machine that builds continuously, a guarded reaper may rarely get a safe window, while the unguarded version destroys real work. That tension is only resolvable upstream, by releasing a conversation's fleet when the conversation ends.macOS: this leak is now reaching OS-level jetsam kills — 12 memory-pressure kill events in 7 days,
largestProcess: codexin 7 of themAnother macOS datapoint, on a build newer than any cited so far in this thread. What I think this adds is not another RSS measurement but OS-attributed evidence of terminal impact: macOS itself recorded the kill events, and named
codexas the largest process in the majority of them.Environment
26.727.51351(build 6119); bundledcodex-cli 0.146.0— newer than the0.146.0-alpha.9.2in the previous comment[mcp_servers.*]blocks in~/.codex/config.toml— 18node, 11npx, 2uvx, remainder native binaries. Mix of vendor-published and in-house stdio servers.[features] multi_agent = true; heavy subagent use — the local thread-history DB records 1,364subagent:thread_spawnrowsEvidence:
/Library/Logs/DiagnosticReports/JetsamEvent-*.ipsmacOS writes a
JetsamEventreport whenever it kills processes under memory pressure. Twelve were recorded in seven days. Parsed:| timestamp | largestProcess | total procs |
nodeprocs | python procs | free RAM ||---|---|---:|---:|---:|---:|
| 2026-07-29 09:02:42 | Code Helper | 7945 | 1885 | 196 | 101 MB |
| 2026-07-29 09:38:07 | Code Helper | 2364 | 1257 | 125 | 170 MB |
| 2026-07-29 13:06:35 | Code Helper | 7585 | 1722 | 177 | 361 MB |
| 2026-07-29 18:39:54 | codex | 7968 | 2135 | 221 | 111 MB |
| 2026-07-29 20:43:01 | node | 2113 | 1143 | 120 | 972 MB |
| 2026-07-30 12:41:04 | codex | 4173 | 2130 | 232 | 174 MB |
| 2026-07-30 12:41:46 | codex | 3522 | 2215 | 242 | 538 MB |
| 2026-07-30 12:47:37 | codex | 15260 | 2253 | 246 | 760 MB |
| 2026-07-30 14:01:24 | codex | 3752 | 2249 | 246 | 326 MB |
| 2026-07-31 19:02:19 | WindowServer | 1949 | 954 | 105 | 117 MB |
| 2026-08-03 16:01:54 | codex | 1894 | 994 | 114 | 847 MB |
| 2026-08-04 17:11:11 | codex | 11226 | 1856 | 216 | 42 MB |
What I think is load-bearing here
1.
nodeprocess count never returns to baseline. Measured on this machine freshly booted with an idle app: 793 processes total, 22node. Across every one of these twelve events,nodesits between 954 and 2,253 — roughly 45×–100× baseline. It does not recover between events; the floor across a week of events is ~1,000, not ~22. That is consistent with the per-thread ownership gap described in the original report rather than with transient spikes.2. The count tracks the MCP server count, not the workload. ~29 node/npx-based stdio servers × roughly 50–70 un-reaped agent contexts lands squarely in the 1,800–2,250 range observed. The python counts (105–246) track the 2
uvxservers on the same multiplier. Both scale with configured server count × spawned threads, which is what you'd predict if each subagent thread starts its own full MCP suite and nothing reaps it.3. This now ends in OS-level kills, not just degradation. The 2026-08-04 event had 42 MB free. macOS named
codexthe largest process in 7 of 12 events.Methodology note — please don't quote GB figures from these files
Summing
rpagesacross all processes in these reports yields 204–230 GB on a 64 GB machine, sorpagesclearly double-counts shared pages across the thousands of processes (each maps the same frameworks). I'm therefore deliberately not reporting a "node used N GB" number — it would be an upper bound at best and I don't want to put a misleading figure into this thread. Process counts and thefreepage count are exact, and those are what I'm reporting above. If a maintainer wants true unique footprint I can capturefootprint/vmmap -summaryon the app-server subtree during a live swarm.Relationship to existing reports
This looks like the same defect as this issue rather than a new one — I checked #12491, #17574, #25015, #25744, #21984 and #20883 before posting and did not open a duplicate. The distinct contribution here is the jetsam attribution and the observation that the
nodefloor never returns to zero.The per-thread MCP pooling proposed in #20883 would address this directly. An
idle_timeoutorshared/reusemode per server, as suggested in the original report, would also bound it. Happy to run instrumented captures on request — I can reproduce this within about 30 minutes of multi-agent work.This is so quickly and reliably reproducible, I now avoid using MCP in general w/Codex. Can you please ack this issue?
Sanitized current-version reproduction and mitigation update (2026-08-06 UTC):
Environment:
0.147.0-alpha.1.2; separately installed stable CLI0.146.0Fresh observation:
codex app-serveraccumulated 284 direct children and 435 total descendants at about 7.5 GB RSSA local config regression had 16 MCP definitions eager, which amplified the leak but does not explain the missing per-conversation cleanup. I reduced eager startup to six while retaining all 26 installed definitions. Both Codex binaries parse that config and report exactly those six enabled. A disposable CLI canary exits with zero surviving MCP roots, while the long-lived ChatGPT app-server that predates the repair continues to show multiplication.
No ChatGPT restart was forced. I will treat the next natural reconnect separately from the upstream fix and will not call this resolved until a current build proves that conversation-close tears down each conversation MCP fleet. Local mitigation PR: https://github.com/lightcloud00/claudecode-workspace/pull/1037.
Same leak, additional (more severe) failure mode on Linux: fd exhaustion →
os error 24, app-server hard-wedgesCorroborating this on Codex CLI 0.144.0 (Linux) with a long-lived
codex app-server(code_mode_host). Same root cause you identified — MCP children parented to the app-server, never reaped when a thread/session ends — but on Linux the file-descriptor ceiling is hit long before RAM becomes the problem, and the consequence is a hard functional outage, not just memory bloat.What we saw: a 28-day-old
codex … app-server --listen unix://had accumulated 224 orphaned live MCP children (148 python MCP + 75headroom, all withPPID = app-server, idle, ages up to 27 days). Each child pins 4 fds in the app-server parent: 1pidfd+ 3 stdiopipes. The fd table at the moment of failure:Once pinned at the fd limit the app-server can no longer create the pipes to fork any subprocess, so:
exec_command failed …: Failed to create unified exec process: No file descriptors available (os error 24)control socket accept error: No file descriptors available (os error 24)The app-server is then 100% unusable (all execs fail) while still showing as "running". Recovery is a manual subtree restart:
pkill -P <pid>; kill <pid>— it self-respawns clean.Why it bites fast on the Linux CLI / SSH path: the
codex app-server daemonbootstrapped "for SSH-driven use" lands in a logind session scope and inherits the OS default softRLIMIT_NOFILE = 1024. At ~4 fds per leaked child that wedges after only ~200 sessions — well before the RSS growth you measured would matter. The daemon's fd limit is also non-deterministic: we measured the same daemon come up at soft1024(SSH/logind path),65536, or1048576(tmux path) depending on which client happened to spawn it first.Suggested fix (in addition to reaping children, which is the real fix): set a sane explicit
RLIMIT_NOFILEon the app-server daemon at startup, so the fd-wedge failure mode is deterministic and far away regardless of the launching client's inherited limit. But reaping MCP children at thread/session end (as you propose) fixes both the RSS and the fd variants at the source — and note that raising the ceiling without reaping simply converts the fd-wedge into the OOM you're already seeing.Happy to share the full
/proc/<pid>/fdbreakdown andps --ppid <app-server>dump if useful.Another macOS datapoint, on stable
codex-cli 0.147.0— currently the latest on npm, and newer than the stable builds cited so far in this thread (0.146.0,0.147.0-alpha.1.2). The leak is unchanged.Environment
codex-cli 0.147.0, installed as the npm global@openai/codexDarwin 25.5.0 arm64, 10 cores, 32 GB swap ceilingapp-servermode. Three independent host sessions, each with its own long-livedcodexprocess.[mcp_servers.context7]→npx -y @upstash/context7-mcp[plugins."context-mode@context-mode"]→ itsstart.mjsObserved (2026-08-10)
| codex PID | uptime | direct MCP children |
|---|---|---|
| 13069 | 20h 33m | 240 |
| 78018 | 1d 14h | 30 |
| 59073 | 1d 16h | 32 |
424 total descendants, 417 of them MCP server processes, 11.5 GB RSS.
System impact: swap 32.8 GB / 33.8 GB used, 246M pageins, load average 427 on 10 cores. The symptom presented as terminal input lag — typing in Ghostty was visibly delayed — which sends you investigating the terminal rather than codex. Ghostty itself was a normal 123 MB.
I measured the accumulation and the parentage only; I did not independently verify the per-thread ownership mechanism, so I'm reporting this as consistent with what's already described here rather than as confirmation of it.
Non-destructive workaround
pkill -P <codex-pid>also kills the servers the live session is currently using. To reap while keeping sessions working: walk the descendants of each stale codex PID, filter to the MCP server commands, keep the newest one of each server type per parent, then TERM and KILL the rest.Result: all three codex processes and their host sessions stayed up, 411 processes reaped, 11.5 GB reclaimed. Load fell 427 → 242 within minutes and swap dropped 32.8 GB → 15.7 GB.
Two small notes for anyone else diagnosing this on macOS: BSD
pshas noetimes, onlyetime(DD-HH:MM:SS), and at 1000+ processes it's worth dumpingpsto a file and parsing it rather than piping throughgrep.Independent Windows reproduction on the current Desktop package, adding controlled lifecycle evidence.
Environment
OpenAI.Codex_26.803.10989.0_x6426200node_replenabledObservations
node.exeand 104node_repl.exeprocesses, with roughly 8–12 GiB aggregate Codex/ChatGPT working set.node+ 77node_repl, using approximately 5.46 GiB.nodeandnode_replprocesses as direct children ofcodex.exe.node, onenode_repl, and one Codex app-server process.Controlled minimal test
A built-in Explorer agent was asked to run only
Get-Date:node=5,node_repl=3node=6,node_repl=4Computer Use initialization also increased the
node_replpopulation. This establishes that agent/tool execution-context initialization can trigger additional runtimes, but it does not prove Explorer is the only trigger or that every process pair maps one-to-one to a thread.A current snapshot on package
26.803.10989.0still showsnode=18andnode_repl=17.Impact and lifecycle boundary
The accumulated processes are mostly low-CPU individually, but together retain substantial memory, threads, handles, and child-runtime state. Manually terminating old, verified pairs reduced resource use, but did not always remove all system-wide stutter; there appears to be a second kernel/system-resource component tracked in #29436.
The full Windows reboot recovery, combined with the failure of an app-only restart to reap these children, is consistent with missing or incomplete app-server child lifecycle management.
Requested fix
I can provide sanitized start-time/count tables and before/after samples privately. I am intentionally not publishing raw session logs or local paths.
Current macOS snapshot — Desktop 26.803.61601
A fresh process/log audit still shows substantial per-task helper accumulation:
| Process family | Count | Aggregate RSS |
|---|---:|---:|
|
node_repl| 143 | ~1.80 GiB ||
codex app-server --listen stdio://| 24 | ~1.42 GiB |The structured log also contains 46 failed MCP process-group termination warnings in the current window:
The stable error class is
Failed to terminate MCP process group ... Operation not permitted (os error 1).This snapshot cannot prove that every helper is orphaned because multiple tasks are active, but the helper-to-app-server ratio and repeated failed cleanup attempts provide current evidence that lifecycle cleanup is not reliably completing.
Platform: macOS arm64. Local paths, PIDs, task IDs, account data, and raw logs are omitted.
Still reproduces, and I have code-level pointers into current
mainthat may narrow down where the per-thread lifecycle hook belongs.Environment
| | |
|---|---|
| Codex desktop app | 26.814.41407 (bundle 6720) |
| codex-cli (bundled) | 0.148.0-alpha.15 |
| macOS | 26.5.2 (25F84), arm64, 48 GB |
That is six minor versions newer than the 0.142.3 in the original report, so this is not fixed.
Same shape, measured growth rate
Four stdio servers configured (a browser-automation server and a search server via
npx, a Python stdio server viauv, plus the bundlednode_repl). Single app-server, ~7 h uptime, observed twice about an hour apart:| | at ~6 h uptime | at ~7 h uptime |
|---|---|---|
| Processes in app-server tree | 307 | 449 |
| Instances of each configured server | 26 | 40 |
The per-kind counts move in lockstep — 40
node_repl, 40uv, 40 of eachnpxserver — which is what one full set per thread predicts. No individual server leaks: each instance sits at 40–100 MB regardless of whether it is 30 seconds or 7 hours old. The fault is process count, not per-process growth.One caveat on the RSS figures in this thread: I read 12.3 GB for the tree at 6 h, then 7.75 GB at 7 h with more processes, because by then 16 GB of swap was in use and 1.9M pageouts had occurred. RSS under-reports this bug once the machine starts paging; process count is the more honest metric.
How much of a turn is MCP
Isolating a single non-interactive turn (prompt: "reply with the single word OK"), peak tree RSS:
| | |
|---|---|
| Whole tree | 1306 MB / 14 procs |
|
codexbinary itself | 263 MB || MCP servers | 1043 MB (80%) |
So even one healthy thread is mostly MCP, which is why per-thread accumulation escalates so fast.
Code pointers (workspace at
ccf4191)Reading current
main, the kill path looks correct and the retention looks like the actual defect:codex-mcp/src/runtime.rs—McpRuntimeis documented as "Owns all mutable MCP state for one Codex thread", so the per-thread fan-out is by design.rmcp-client/src/stdio_server_launcher.rs:283,291— the launcher already setskill_on_drop(true)andprocess_group(0). Dropping the connection set should reap the process group.core/src/session/handlers.rs:417—mcp_runtime.shutdown()lives inshutdown_session_runtime, which is only reached viaOp::Shutdown(handlers.rs:662).core/src/thread_manager.rs:1071—remove_threadisthreads.write().await.remove(thread_id). It drops the map entry but never callsshutdown_and_wait().core/src/thread_manager.rs:1098—shutdown_all_threads_boundedis the only path that shuts every thread down, i.e. app exit. That matches the observation in #38925 that killing the app-server does reap everything.Net effect: because the
Arc<CodexThread>stays reachable for the app-server's lifetime,Dropnever runs, so the perfectly goodkill_on_dropnever fires.A possible hook for the cap requested above
The suggested fix in this issue asks for an
idle_timeout/shared/reusemode. Worth noting that a residency cap already exists — but only for one class of thread:core/src/agent/control/residency.rs—reserve_v2_residency_slotbounds resident V2 subagent threads againsteffective_agent_max_threads, evicting viatry_unload_one_residentwhen the cap is hit.is_v2_resident_session_sourcescopes it to V2 subagent sources.If that machinery were generalized to conversation threads, it would give exactly the bounded-residency behaviour this issue asks for, without needing a new lifecycle subsystem.
Interim workaround
Beyond the
pkill -Pin the original report, the cost is multiplicative in configured servers, so pruning globalmcp_serversto the ones actually used daily and preferring remote (url = "...") servers over stdio helps a lot — remote servers cost no local process per thread. Restarting the app remains the only complete reclaim.Controlled macOS A/B result on Codex Desktop
26.814.41407(bundle6720), bundledcodex-cli 0.148.0-alpha.15.Before the change, four global STDIO MCP servers were enabled: Playwright, two SSH MCP servers, and the bundled
node_repl. New internal/thread sessions started matching batches in lockstep. The live app-server accumulated 8 Playwright instances, 8node_replinstances, and two SSH processes per batch. The ChatGPT/Codex process tree reached approximately 5.6 GB RSS.I then set
enabled = falsefor Playwright and both SSH MCP servers, keptnode_replenabled, fully quit the desktop app with Cmd+Q, and restarted it.After restart:
node_replprocesses: 2, stableThis indicates that globally configured STDIO servers were a multiplicative amplifier, not the underlying defect. Disabling them removes most of the immediate memory growth, but the core lifecycle issue remains: compatible local MCP servers are created per thread/session and retained until the app-server exits.
The observed behavior supports either sharing compatible STDIO connections across threads or explicitly shutting down the thread MCP runtime when a thread is removed or unsubscribed.
Correction: my earlier five-minute sample was too short. Across separate Desktop sessions,
node_replgrew from 2 to 9 instances and Codex RSS increased from ~2.6 GB to ~3.5 GB, while Playwright and SSH remained disabled.Codex Desktop also re-enabled its bundled
node_replafter restart. Disabling custom MCP servers reduces the impact but does not fix the underlying lifecycle leak.I reproduced this as a stdio MCP lifecycle issue and prepared a tested patch here: https://github.com/goodyang227/codex/pull/1
Root cause: per-thread runtime/catalog ownership kept stdio client bindings alive after a turn became idle. Refreshing the runtime could create replacement stdio sessions while the old Arc-owned bindings (and their subprocess groups) remained retained.
The patch:
Regression coverage includes repeated idle cleanup/restart, in-flight call safety, optional slow startup, required restart failure combined with forced pre-sampling compaction, MCP tool cache/exposure behavior, and the codex-mcp suite. Focused results: lifecycle 4/4, pre-sampling compact 13/13, tool cache 5/5, tool exposure 20/20, codex-mcp 192/192.
This is a fork patch, not a released Codex build; it has not been deployed to the installed Desktop app.
the reuse/shared mode you floated at the bottom quietly swaps a resource leak for a correctness bug. playwright and node_repl aren't stateless, so one shared process means thread A's browser context, open pages, and the live kernel show up in thread B. two concurrent threads clobber each other's navigation and cookies, and it reads as flaky output, not a crash. stateless servers can share fine, but the ones eating your RSS (playwright, davinci-resolve) are exactly the ones that can't, so this probably wants a per-server stateful flag rather than a blanket shared pool. idle_timeout has the same edge if it reaps a process holding warm auth mid-run.