Repository navigation
Codex.app GUI: MCP child processes not reaped after task completion — 1300+ zombies, 37GB memory leak #12491
Description
Codex.app GUI: MCP child processes not reaped after task completion — 1300+ zombies, 37GB memory leak
What version of Codex CLI is running?
codex-cli 0.98.0 (Codex.app GUI wrapper)
What subscription do you have?
ChatGPT Pro
Which model were you using?
o3 (via Codex.app GUI)
What platform is your computer?
macOS 26.3 (25D125), Apple M4 Max, 64GB RAM, arm64
What issue are you seeing?
Codex.app spawns codex exec --full-auto worker processes for each worktree but never reaps them or their MCP child processes when tasks complete. Over multiple audit sessions, this accumulated 1,319 zombie Codex processes and 1,537 node processes consuming ~37GB RSS + 40GB swap on a 64GB machine, causing severe system-wide slowdown.
Root cause analysis
Each Codex.app task creates:
- 1×
codex exec --full-auto --skip-git-repo-check --json -(CLI worker) - 4×
oh-my-codexMCP servers (state, memory, trace, code-intel) - 1× Serena MCP server (via
uv tool uvx) - 1×
uvprocess for Serena
≈ 7 processes per task. None are killed when the task completes.
With 12 git worktrees created by Codex.app (detached HEAD, pointing at old commits), this produced:
- 12 worktrees × ~110 child processes = ~1,320 sleeping processes
- Combined RSS: ~37GB (mostly npm/node/uv MCP servers)
- Swap usage: 39.4GB (on 64GB physical RAM)
When Codex.app (PID 19947) was killed, 2 additional codex exec workers (launched Friday and Saturday) were reparented to launchd (PPID=1), confirming they were never properly cleaned up.
Process tree snapshot (before cleanup)
Codex.app (PID 19947, 279MB)
├── codex exec --full-auto (PID 19970, 123MB)
│ └── 880× node/npm/uv children (27GB total)
│ ├── npm (236MB each, ×2)
│ ├── npm (140MB each, ×4)
│ ├── uv (91MB each, ×2)
│ └── node (77MB each, ×dozens)
├── Codex Helper Renderer (PID 20018, 602MB)
└── Codex Helper (PID 19971, 101MB)
Orphaned (PPID=1, reparented from dead Codex.app sessions):
├── codex exec --full-auto (PID 59326, Fri 23:59)
│ ├── oh-my-codex/state-server.js
│ ├── oh-my-codex/memory-server.js
│ ├── oh-my-codex/trace-server.js
│ ├── oh-my-codex/code-intel-server.js
│ └── serena start-mcp-server
└── codex exec --full-auto (PID 59747, Sat 22:05)
└── (same MCP stack)
Memory impact
| Metric | Before cleanup | After cleanup | Delta |
|---|---|---|---|
| Swap used | 39.4 GB | 8.0 GB | -31 GB |
| Codex processes | 1,319 | 0 | -100% |
| Node processes | 1,537 | 17 | -99% |
| Free pages | 82K | 2.3M | ×28 |
Worktree state at time of discovery
All 12 worktrees were detached HEAD on old commits (2+ phases behind current main), confirming tasks had completed long ago:
/Users/drg/.codex/worktrees/0585/medical-ai 627d44e (detached HEAD)
/Users/drg/.codex/worktrees/0b7b/medical-ai 627d44e (detached HEAD)
/Users/drg/.codex/worktrees/2a60/medical-ai 4d53e7d (detached HEAD)
... (12 total, all on 2 old commits)
Current HEAD: 71aefff — none of the worktrees were anywhere near current.
What steps can reproduce the bug?
- Use Codex.app GUI to run multiple tasks on a git repository (especially with MCP servers configured via oh-my-codex)
- Let tasks complete normally
- Wait a few hours / run more tasks
- Check process count:
ps aux | grep -c codex - Check memory:
ps aux | grep codex | awk '{sum+=$6} END {printf "%.0f MB\n", sum/1024}'
Accumulation is proportional to number of tasks × MCP servers configured.
What is the expected behavior?
When a Codex.app task completes:
- The
codex execworker process should be terminated - All MCP child processes (oh-my-codex servers, Serena, uv) should be killed via process group signal
- The git worktree should be cleaned up if the task is done (or at minimum, child processes should not persist)
What is the actual behavior?
- Worker processes and their entire MCP subtree remain alive indefinitely
- No cleanup occurs on task completion, app quit, or even app crash (reparented to launchd)
- Memory grows unboundedly with each task
Relationship to existing issues
- CLI-494 (fixed, Oct 2025):
start_kill()withoutwait()inexec.rs— same root cause pattern (missing process reaping), but fix only covered timeout/Ctrl+C in CLI, not GUI lifecycle - CLI-3017 (Jan 2026): CLI memory leak causing crashes
- #12414 (Feb 2026, open): Unbounded memory growth when idle on v0.104.0
This issue is distinct because it specifically affects Codex.app GUI and its interaction with MCP server child processes — the GUI spawns long-lived codex exec workers that themselves spawn MCP servers, creating a deeper process tree that the existing cleanup logic does not handle.
Suggested fix
- Process group kill:
codex execworkers should usesetsid()/ process groups so all children (MCP servers) can be killed with a singlekillpg()on task completion - Worktree lifecycle: Codex.app should track active worktrees and clean up stale ones on startup (e.g., remove worktrees whose tasks are no longer active)
- Heartbeat/watchdog: MCP servers spawned by
codex execshould monitor their parent and self-terminate if the parent dies (check PPID periodically, or use a pipe/fd that breaks on parent death)
Activity
github-actions commentedon Feb 22, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
vincentkoc commentedon Feb 22, 2026
I put up a draft fix for this path: #12505
Scope is intentionally narrow and low-risk:
features.thread_listener_cleanupas an experimental flag (default off)I am currently seeing memory pressure in app usage with 5+ concurrent threads, so this is aimed at reducing retained in-memory thread/listener state without changing default behavior.
vincentkoc commentedon Feb 22, 2026
Given the updated contribution policy (invitation-only PRs), #12505 is currently a draft patch proposal to make discussion concrete. If this direction aligns with maintainer intent, would someone from the team be willing to explicitly invite that PR (or suggest a narrower scope)? I can provide additional repro/profiling details from macOS sessions where memory pressure climbs with 5+ concurrent threads.
gabrielbryk commentedon Mar 1, 2026
Experiencing this on Linux / codex-cli 0.106.0
Reproducing the same issue. Here's a breakdown of the two distinct leak patterns I'm seeing:
Environment:
Crash:
failed to spawn thread: Os { code: 11, kind: WouldBlock, message: "Resource temporarily unavailable" }Two distinct leak patterns observed:
Zombie children not reaped (missing
waitpid): A single long-running codex session accumulated 41 zombie<defunct>children — all from a Python stdio MCP server. The MCP server forks subprocesses for tool calls, but the codex parent never callswaitpid()to reap them.pstreeoutput:MCP server processes not terminated on session exit: When a codex session ends or is interrupted, its stdio MCP child processes are never sent SIGTERM. They persist indefinitely. I found 8 separate MCP process trees (one per stale codex session), each still running. These are live processes consuming real resources, not just zombies.
The combination of both leaks led to thread exhaustion and the crash. This appears to be the same root cause as #4726 (closed) and #6201 (closed) — neither of which fully resolved the lifecycle management for stdio MCP servers.
masvelio commentedon Mar 11, 2026
I can confirm a very similar issue on macOS, and I captured a full CLI process snapshot on March 11, 2026.
Environment
26.305.95014.3.1 (23D60)context7,stripe,playwrightWhat I observed
607total processes5760threads15G used,74M unused6608M compressor6342612swapins8519545swapoutsapp-serverPID:codex app-serverPID6030660306:7372were MCP launchers24 x npm exec @upstash/context7-mcp24 x npm exec @stripe/mcp --tools=all24 x npm exec @playwright/mcp@latest148processes totalThis does not look like a bounded “currently active work only” baseline. It looks like repeated MCP launcher accumulation over time under the same long-lived
codex app-server.Long-lived duplicated MCP launcher examples
8hold:npm exec @playwright/mcp@latestnpm exec @stripe/mcp --tools=allnpm exec @upstash/context7-mcp4hold:playwright/stripe/context73hold:1hold:50mold:20h+old:app-serverImportant distinction
#12333that duplicate MCP instances may be expected while subagents/threads are active.codex app-serverPID over many hoursThis feels closely related to:
#12491#11324#12976#10710Question for maintainers
#10710have covered this desktop app path as well?Thanks for taking a look. The snapshot seems to point to unbounded MCP process accumulation over time rather than short-lived fanout from currently active work, so hopefully it helps narrow the cleanup path.
RemarkRemedy commentedon Mar 16, 2026
This looks related to
#11324, but I wanted to add a narrower app-server angle that may help isolate root cause.I traced a reproducible case where the leaking MCP children are not orphaned from dead workers; instead they remain attached to the long-lived desktop
codex app-serverprocess after conversations are no longer active.The key detail from source review is that MCP servers are conversation-scoped and the intended unload path is
thread/unsubscribe. When the last subscriber unsubscribes, the app-server shuts down the thread and unloads it. However, on connection removal the current app-server code path retains zero-subscriber threads instead of unloading them, so theirMcpConnectionManagers never drop and the stdio MCP child processes stay alive.I posted the fuller write-up and evidence here:
#11324comment: MCP servers eat up memory when multi-tasking in Codex App #11324 (comment)So this may be the same family of leak as this issue, but specifically through desktop thread lifecycle / unload behavior rather than only worker reaping.
39 remaining items
fix(mcp): reap superseded MCP connection managers on refresh
bbingz commentedon Jul 24, 2026
Analysis (community)
Thanks for tracking this — attempting a residual-focused analysis aligned with docs/contributing.md (invitation-only: analysis in-thread, no unsolicited PR).
process-leak)Observed (from issue + public residual signal)
Codex.app GUI: MCP child processes not reaped after task completion — 1300+ zombies, 37GB memory leak ## What version of Codex CLI is running? codex-cli 0.98.0 (Codex.app GUI wrapper) ## What subscription do you have? ChatGPT Pro ## Which model were you using? o3 (via Codex.app GUI) ## What platform is your computer? macOS 26.3 (25D125), Apple M4 Max, 64GB RAM, arm64 ## What issue are you seeing? Codex.app spawns
codex exec --full-autoworker processes for each worktree but never reaps tRoot-cause hypothesis
MCP/Node helper processes retained after task completion (GUI path especially) accumulate CPU/RAM; aligns with desktop OOM reports.
High-level fix outline (not a PR)
Related
#12491 and desktop memory cluster (#33582, #32797).
Questions for maintainers
Happy to refine with more measurements or code pointers. No unsolicited PR from me.
bbingz commentedon Jul 24, 2026
Analysis (community)
Residual analysis for #12491 (GUI: MCP child processes not reaped) — invitation-only. Long-open process-lifecycle residual; distinct from Windows WMI/taskkill mega-threads (not pile-on of denylisted storms). No unsolicited PR.
Deterministic evidence (code)
Code (
5dd992acd3):codex-rs/mcp-server/tests/common/mcp_process.rstokio::process::Childwith.kill_on_drop(true)(≈L82)codex-rs/codex-mcpconnection manager (connection_manager/rmcp_clientre-exports) — lifecycle of external Node/stdio MCP servers must be joined on task end and session close.codex_mcp::connection_manager≈1745 TRACE rows undercodex-cli 0.146.0-alpha.6sessions on this host (indirect signal of frequent connect/disconnect churn).Root-cause hypothesis
Task completion / GUI session teardown does not always wait on MCP child process groups.
kill_on_dropin tests acknowledges cleanup is best-effort; production paths that spawn helpers without a guaranteed join accumulate orphans → RAM/CPU growth over long Desktop sessions (adjacent symptoms on #33582 / #32797, different platforms).High-level fix outline (not a PR)
waiton task end, thread close, and app-server shutdown.Questions for maintainers
codex-mcpconnection manager?No unsolicited PR from me.
yanxiaolong01 commentedon Aug 5, 2026
Title:ChatGPT macOS app leaks zombie (defunct) child processes — exhausts
per-user process limit
App version: ChatGPT 26.730.61639 (Codex Framework 151.0.7922.71)
OS: macOS 14 (Darwin 23.2.0), Apple Silicon
Description:
The ChatGPT desktop app continuously accumulates (zombie) child
processes without reaping them. The parent is the main ChatGPT process, and
the leak traces to the embedded Codex app-server / code-mode-host chain
(codex app-server → node_repl / codex-code-mode-host).
Rate: ~36 zombies/minute. After ~10 minutes of runtime the app held 400+
zombie processes.
Impact: On macOS the per-user process limit (kern.maxprocperuid) is 2666. At
this leak rate the app exhausts the process table in roughly an hour, after
which the entire system fails to fork new processes — every shell and app
throws fork failed: resource temporarily unavailable until ChatGPT is quit.
Quitting ChatGPT lets launchd reap all zombies instantly, confirming the app
is the sole cause.
Root cause (likely): child processes are fork()ed but the parent never calls
wait()/waitpid() to reap them.
Steps to reproduce:
code-mode/agent host spawns subprocesses).
periodically.
Expected: child processes are reaped; zombie count stays near zero.
Actual: zombies grow unbounded until the OS process limit is hit.
bbingz commentedon Aug 6, 2026
@yanxiaolong01's process chain narrows the ownership question I raised above:
codex app-server → node_repl / codex-code-mode-hostis the code-mode host spawn path, not the stdio MCP server path thatcodex-mcp's connection manager owns. ~36 zombies/min with the parent still alive means the children exit and are neverwait()ed — a missing reap on the host side, not a kill/teardown ordering bug.Two things that would make this actionable:
ps -o pid,ppid,stat,command -p <zombie-pid>for a handful of the defunct entries, plus the PPID chain. Whichever process is the direct parent owns thewait(), and that decides where the fix goes.node_replchildren explicitly, or does it rely on drop-time kill the way the test helper does (codex-rs/mcp-server/tests/common/mcp_process.rs,.kill_on_drop(true)≈L82, with the "not a guarantee" comment at ≈L39-41)?Fail-first test unchanged: spawn a code-mode cell, complete the task, assert zero defunct children under the app-server PID. No unsolicited PR from me.
yanxiaolong01 commentedon Aug 6, 2026
Requested ps data (ChatGPT 26.730.61639 / Codex Framework 151.0.7922.71,
macOS 14.2 23C64, Apple Silicon)
Ran the scoped
psyou asked for. The direct parent of the zombies is NOTthe app-server — it's the top-level native host process.
Zombie sample (
ps -axo pid,ppid,stat,command):71372 70992 Z
71442 70992 Z
71445 70992 Z
PPID chain:
<zombie 71372> Z --PPID--> 70992 --PPID--> 1 (launchd)
70992 S /Applications/ChatGPT.app/Contents/MacOS/ChatGPT # top-level
native host, PPID=1
71348 S /Applications/ChatGPT.app/Contents/Resources/codex -c
features.code_mode_host=true app-server --analytics-default-enabled #
PPID=70992
Zombie parent distribution (own zombies only):
949 70992 <- ChatGPT top-level native host (the leak)
19 1506 <- Google Chrome (unrelated 3rd party)
3 70835 <- GoogleUpdater (unrelated 3rd party)
Key finding that scopes the fix:
codex ... code_mode_host=true app-server(PID 71348) currently haszero child processes — it is not the direct parent of any defunct.
ChatGPTnative host (70992), whosePPID=1.wait()lives in the top-level host that actually spawns thecode-mode
nodechildren, not inside the app-server / MCPconnection-manager layer.
Rate: 70992-owned zombies went 972 → 981 in 15s (~36/min), parent stays in
state
S(alive) the whole time — confirms a pure missing-reap, not akill/teardown-ordering bug.
hoklims commentedon Aug 6, 2026
Windows evidence: the leaked children are NOT orphans, and lifetime CPU cannot identify them
Adding a Windows data point that differs from most of the thread in two ways that I think are actionable. Cross-references #22992 (duplicate MCP servers across conversations) and #30753 (duplicate pools after
RefreshMcpServers), which describe the same shape.Environment
26200.8973, 63.7 GiB RAMOpenAI.Codex_26.730.8199.0_x64codex.exe ... app-server --analytics-default-enabled, launched byChatGPT.execonfig.toml1. Every leaked process had a LIVE agent ancestor
I walked the full ancestry of all 605 MCP/language-server processes rather than checking the direct parent. All 605 resolved to a live
claude.exeorcodex.exeancestor. Zero were orphaned.A parent-is-dead check finds nothing here, which is why this can look like "no leak" to tooling that only looks one level up. The processes are attached to a live
app-serverand simply never serve again.Attribution by owning root:
claude.exe#79308claude.exe#43492claude.exe#38668codex.exe#81384Totals:
codex#81384 owned 454 of the 605. The three Claude Code sessions owned 40–60 each.2. Spawn timeline: complete fleets, repeatedly, never reaped
33 processes = one full fleet of my 16 servers. Creation timestamps for
codex#81384's direct children:Roughly 13 fleets in about an hour. No previous fleet was terminated.
3. The discriminator: lifetime CPU is useless, windowed CPU is not
This is the part I have not seen stated in the thread, and it matters for anyone writing cleanup logic.
Several of my servers load an ML model at startup. Each
vault-semanticinstance burns 9–13 s of CPU importing torch and loading BGE-M3 before it ever listens. So a "has it done work?" test based on lifetime CPU classifies 7.5 GiB idle duplicates as busy and keeps them.Sampling CPU over a live window instead is blind to startup cost:
Example of the two figures disagreeing on the same process:
4. Cost
vmmemWSLand twopython.exeat 7.86 GiB eachTerminating all-but-newest per (root, server), restricted to processes idle over a live window, reclaimed 93.9 GiB and took commit from 95 % to 42 %. Nothing broke: the newest instance under each root is the one the session is actually using.
5. Suggestion
Whatever the eventual fix (#20883's project-scoped pool would remove the class entirely), a teardown heuristic that keys on liveness of the parent will not catch this, and one that keys on lifetime CPU will keep the most expensive offenders. Keying on "newest instance per (owning agent, server)" plus a windowed idleness check identified 499 of 669 processes correctly here with no false positives.
Happy to provide the raw per-process table or re-run any specific measurement.
yanxiaolong01 commentedon Aug 7, 2026
Environment
Verification
Tested on the latest version (26.803.41515) with real-time
monitoring:
Conclusion
Issue Resolved: Version 26.803.41515 no longer exhibits
the zombie subprocess accumulation issue.
etraut-openai commentedon Aug 7, 2026
This thread conflates several different problems. That makes it difficult to tell which of these have been fixed and which still exist. I can say with high confidence that the original bug reported in this thread was fixed, which is why I initially closed the issue. It looks like it was reopened at some point because similar symptoms were reported. There is at least one other bug related to
ProcessHandle::terminate()that is a real bug, and I'll work on a fix for that one.If you are seeing other bugs that involve process leaks, please open a new issue and provide details so we can track it independently.
bbingz commentedon Aug 7, 2026
Thanks @etraut-openai — agreed this thread conflated several process-lifecycle failures.
My earlier residual notes were aimed at that same split (owned-process reaping /
ProcessHandle::terminate()vs the original Desktop zombie report). Standing down here; will only open a new issue if I can reproduce a distinct residual with a clean repro.etraut-openai commentedon Aug 7, 2026
The
ProcessHandle::terminate()issue will be addressed in the next release, so I'm going to close this. As I mentioned above, if you see similar symptoms, please open a new bug report. Thanks!bbingz commentedon Aug 7, 2026
Thanks @etraut-openai — appreciated the split clarification and the upcoming
ProcessHandle::terminate()fix. Standing by for the next release; will only open a fresh issue if a distinct residual shows up with a clean repro.berkkorkmaz commentedon Oct 2, 2026
One pattern I saw: leftover helpers from an older app instance (
cua_node/bin/node … kernel.js) haveCODEX_SANDBOXset but no thread id, their parent is launchd and their group leader is gone. Those three together are a pretty safe way to spot them. Just checking "parent is launchd" flags hundreds of normal processes on macOS.I put this in a read-only tool if it helps: https://github.com/berkkorkmaz/tidewake