MCP servers eat up memory when multi-tasking in Codex App
What version of the Codex App are you using (From “About Codex” dialog)?
260208.1016
What subscription do you have?
Business
What issue are you seeing?
Hey,
I'm working 100% of the time using the Codex app, working on multiple parallel worktrees overtime multiple days, can get to more than 10 active threads at a time.
I also have 17 MCP servers configured for my app.
Combine these two and you get about 30gb of memory used when the codex app is open on my PC, simply because i have 10 codexes working in parallel, not to mention sub agents and such.
Further more, when I close off/archive conversations in the codex app, it doesn't seem to kill the conversation process as well as MCP server processes, effectively flooding the PC with stale instances that fill up memory overtime.
A few solution I can propose to address this:
- if usage is through the app, share MCP connections across all codex instances. I understand it's very different from how Codex functions today but it's one approach
- Implement tool search so context doesn't bloat + MCP servers are connected at boot up so only the needed MCPs are used with how the conversation progress
- Allow turning off, turning on and restarting MCP servers incase some connections aren't working and the process is stale on the PC
What steps can reproduce the bug?
Work on multiple codex conversations in parallel
What is the expected behavior?
High memory usage on MAC
Additional information
I am using the MacBook Pro M4 Max with 36GB of Ram, so it took a lot of Codex session to reach the limit of my PC, but eventually it did.
15 Comments
With that many MCP servers, I'm surprised codex is even functioning. MCP servers can take up quite a lot of your context window (due to their instructions / meta-data / schema etc). Honestly, in my experience codex is just phenomenal without external assistance. It should not need that many MCP servers unless all of these are custom MCP servers that control gadgets around your house.
@guidedways it works very well. compaction handles context fill up really smoothly in the transition, and i do need some of the MCPs each time, anything i don't need i remove.
when i'm doing research i need to connect a lot of MCPs to gather context.
I've also experienced that codex does not properly clean up the mcp servers it started up when running. That includes mcp servers started up by the sub-agents as well
bump? :)
Related issues that may help narrow root cause and mitigation:
These are CLI-focused, but the underlying lifecycle pattern seems aligned with what this App issue reports (stale MCP/agent processes accumulating over time).
If useful, we can consolidate around:
1) MCP connection sharing/pooling strategy,
2) explicit auto-close lifecycle policy for completed/idle agents,
3) deterministic MCP teardown when no active agent references remain.
I encountered the same problem.
I dug into this locally and can confirm a more specific app-side failure mode that matches the original report.
My setup:
26.311.21342 (build 993)Darwin 25.3.0 arm64 armomx_state,omx_memory,omx_code_intel,omx_trace, and a customclaude_plan_reviewWhat I observed:
Codex.app/Contents/Resources/codex app-serverlaunchd; they stay attached to the still-running app-server and accumulate over timeA representative process snapshot looked like this:
Source-level findings from
openai/codex:ThreadManager::spawn_thread_with_source()goes throughCodex::spawn(), which creates a freshMcpConnectionManagerfor that session.kill_on_drop(true)and a process-group guard that terminates / kills the process group on drop.thread/unsubscribe. If the last subscriber unsubscribes, it shuts down the thread and emitsthread/closed.retaining thread listener after connection disconnect left zero subscribersImportant control experiment:
/dev/nullMy current read is:
McpConnectionManagers never dropThis seems closely related to
#12491, but the narrower root cause here is conversation / thread unload lifecycle in the desktop app, especially aroundthread/unsubscribeand disconnect handling.Same pattern here, plus residue after Cmd+Q
@yahelroro0196 this matches what I saw. @francoisrob on sub-agent cleanup and @RemarkRemedy on the app-server unload path also line up with my side.
My setup
26.311.21342 (build 993)Darwin 25.3.0, Apple Silicon18conversations over~2.5 dayscontext7,playwright,chrome-devtools,webflow mcp-remote, custom Python MCPBefore I quit the app
It looked like each conversation got its own full MCP bundle, and finished/archived conversations were not going back to a sane baseline.
I want to be clear on the same distinction people made in
#12333and#14233: i am not asking for one shared MCP instance for all active conversations. The bug on my machine was teardown after the work was already done.After Cmd+Q
I quit Codex from the Dock on March 17, 2026. Most of the pile died right away:
But cleanup was still not complete. I still had:
The old survivors matched earlier Codex sessions from
March 9, 2026andMarch 14, 2026.So for me this is not only "memory gets high while Codex is open". It is also "a small orphan set can survive the quit and still be there across later sessions".
Same-machine control:
#10710is why this feels like an app lifecycle gap more than a raw stdio cleanup bug. The process-group cleanup seems to exist when drop happens. The desktop app path still looks like it sometimes never reaches that clean drop for every conversation.One architecture split that has helped us on a similar runtime: treat MCP child lifetime as a separate admission problem from thread lifetime.
Concretely:
owner_threads, last tool call timestamp, rss, idle_since)refcount == 0plus a short idle TTL, instead of tying the child to whether the parent app-server is still aliveThat tends to solve two different pain points at once:
If the team does not want global MCP sharing, a narrower fix would still help a lot: deterministic teardown on zero live thread refs, plus a visible
why_alivereceipt in the app so operators can tell stale from intentional.We have been hitting the same class of failure in MartinLoop, and the biggest win was making teardown observable before trying to make it clever.
Reproduced this on the current Codex Desktop build on Apple Silicon.
A secret-safe process inventory found roughly 900 MCP-related Node processes consuming about 10 GB RSS. The repeated groups were proportional to retained task runtimes (approximately 75 copies of each configured stdio server or wrapper). Many processes had been alive for about 20 hours. Migrating compatible remote servers from stdio bridges to native Streamable HTTP prevented new bridge copies, but existing task-owned processes remained alive until Codex was restarted.
This suggests the missing layer is Codex-side lifecycle ownership, not just individual MCP implementation cleanup.
Possible fixes:
mcp-remote-style bridge duplicates a transport Codex can already speak directly.Account identity and state isolation must remain part of pooling eligibility: two separately authenticated MCP registrations must not be merged merely because they use the same implementation.
I can reproduce this MCP lifecycle/resource retention issue on Codex Desktop for Windows.
Environment
Reproduction steps
Actual behavior
The MCP/runtime processes remain alive after their corresponding turns have completed and the threads are no longer active.
In one reproduction, a single
codex.exeapp-server retained five separatenode_repl.exedirect child processes, started at different times:The resulting process counts were:
All five node_repl processes were direct children of the same live codex.exe app-server. They were not OS-orphaned processes.
The Codex Desktop logs correlate new node_repl process creation with thread resume events. When switching threads, the logs show:
active=falseactive=truethread/resumedisposeAfterSessionActivity=falseIn the inspected log:
disposeAfterSessionActivity=false: 37 occurrencesdisposeAfterSessionActivity=true: 0 occurrencesFully terminating Codex Desktop removes the process trees, but normal turn completion and switching threads do not.
Impact
The number of Node/npm/MCP processes grows with the number of resumed threads. On an enterprise Windows device, endpoint security and Defender inspect these additional process trees, causing sustained CPU usage in security agents, WMI Provider Host, and Defender components.
Restarting Codex temporarily resolves the problem, but the processes accumulate again as threads are resumed.
Expected behavior
One of the following should happen:
This appears related to #11324 and #21984, but this reproduction is specifically from Codex Desktop on Windows/MSIX.
<!-- Failed to upload "image.png" -->
I ran a controlled
/exitvs/archivecomparison and can reproduce a narrower lifecycle boundary on Linux.Environment
0.145.0Linux 7.0.0-28-genericcodex ... app-server --listen unix://Method
For each trial I:
/exitor/archiveThis tracks PID identity rather than relying on aggregate process counts, since other Codex clients were active concurrently.
Results
/exitthread/unsubscribefor that connection./archivecontrolthread/archive, followed byclearing thread listener during thread-state teardown, and graceful SIGTERM exits for Vera, Probe, and Mempalace.Interpretation
The MCP child implementations terminate correctly when their connection manager/thread state is actually dropped. The retention is specifically associated with the resumable
/exit/ zero-subscriber path, whereas/archivedeterministically tears down the thread and its stdio MCP bundle.Whether keeping a resumable thread object is intentional or not, retaining a full MCP process bundle for every exited TUI session explains the long-running app-server memory growth. An idle/LRU disposal policy or dropping MCP managers independently of resumable transcript state would avoid that accumulation.
Follow-up to my earlier
/exitvs/archivereproduction: I traced the retention condition further and prepared a reviewed patch.The narrower root cause is app-server attachment bookkeeping. Every initialized connection is automatically attached to internally created threads so it can receive events and answer server requests. Those delivery-only attachments currently count the same as explicit owners.
/exitremoves the originating TUI subscription, but another long-lived initialized client can keep the retaining count nonzero indefinitely, so the existing 30-minute idle-unload timer never starts.Patch and review artifact:
The patch distinguishes delivery-only
Autoattachments from retainingExplicitowners while preserving event delivery and the full 30-minute grace. Explicit lifecycle requests, normal turns, realtime work, and inline reviews upgrade ownership monotonically. Turn/review admission is serialized against unload commitment, so work either starts with ownership established or is rejected after closing commits. Archive behavior remains immediate.Focused lifecycle, ownership, WebSocket, cold-resume, and inline-review tests pass;
just fix -p codex-app-server,just fmt, andgit diff --checkare clean. The broader app-server run executed 1,026 tests: 1,019 passed, 1 skipped, and 7 initially failed. Building missing companion binaries cleared five; the remaining two are unrelated sandbox-runtime failures where the copied helper cannot loadlibssl.so.3.Adding a mechanism-level data point on a newer build. The symptoms match what's already reported here; what I think is new is (a) log evidence that the gate is subscriber retention rather than the teardown path, and (b) a positive control showing the low-level cleanup does work when shutdown is actually reached.
Environment
26.721.41059(build5848)0.146.0-alpha.3.1Darwin 25.5.0, Apple Silicon, 32 GB RAMbrave-search,context7,qmd,notebooklm-mcp,playwright,node_repl)Host impact
After ~40 hours of app-server uptime:
| | before cleanup | after |
|---|---|---|
| total processes | 1267 | 941 |
| children of
codex app-server| 277 direct / 415 descendants | 0 || wired memory | 27 GB / 32 GB | 3.8 GB |
| free memory | 121 MB | 10 GB |
| swap used | 27.2 GB / 28 GB | 8.9 GB / 10 GB |
| load average (1 min) | 333 | 71 and falling |
Wired memory was the operative number. It cannot be paged out, so once it reached 27 GB the machine went into sustained thrashing (
kernel_taskat 126% CPU). Per-process RSS was small — the 415 descendants held only 68 MB resident, nearly all swapped out — so RSS-based monitoring would not have caught this. Process count and wired memory did.Child process breakdown: 51 ×
qmd, 51 ×@upstash/context7-mcp, 51 ×@playwright/mcp, 46 ×@brave/brave-search-mcp-server, 46 ×node_repl. Elapsed times were distributed continuously from 21 minutes to 1 day 15 hours, i.e. one set accumulating per thread rather than a single burst.What the logs show
From the local app-server log database, for the 40-hour app-server:
thread/closedevents during the entire 40 hours. Then 46 fired at once, immediately after the client connection was lost, together with 46 ×has no subscribers and is idle; shutting down.rpc.method="thread/unsubscribe".So threads stayed
loadedwith a subscriber for the whole session, and the 30-minute idle unload was never reachable.Positive control
On the replacement app-server, two threads did reach idle shutdown, and exactly 12 MCP launcher roots disappeared at the same moment (6 servers × 2 threads). The process-group teardown added in #19753 is working. The defect is upstream of it, in the thread lifecycle:
thread/startauto-subscribes the client,turn/completeddoes not unsubscribe, and nothing releases the subscriber while the client connection stays up.This also means "archive the conversation" does not help, which matches the reports above — archiving does not drop the subscriber.
Secondary issue: app-server not exiting
After the parent app quit, the old app-server survived with
PPID=1and zero children for another ~24 hours, while a new app-server ran concurrently. The two competed and produced repeated remote-control409s until the stale one was killed manually. This is adjacent to #21008 but not the same thing — there the orphans are MCP helpers, here it is the app-server itself.Suggested fixes, in the order I'd expect them to pay off
thread/unsubscribewhen a thread leaves live view. If the frontend already intends to, this looks like a missed call — but I could not confirm either way, since the shipped frontend is minified. The stringthread/unsubscribeis present in the bundle; the call condition is what I could not determine.loadedthreads (LRU), independent of subscriber state.Point 5 is what currently blocks any safe user-side workaround. Because no MCP process carries its owner thread ID, an external reaper cannot distinguish a leaked helper from one belonging to an active thread — I had sets older than 30 minutes and sets that were correctly reclaimed minutes later, side by side.
pkill -P <app-server>clears everything but also takes outcodex-code-mode-hostand any in-flight task, so it is an emergency measure, not something that can be scheduled.The only mitigation I found that is both safe and effective is reducing the number of globally enabled stdio servers, since cost scales with (enabled servers × loaded threads). Note that
enabled_tools/disabled_toolsdo not help — the server still starts at session init; onlyenabled = falseon the server itself prevents the process.Adding a 16 GB macOS data point from the current build. This reproduces very quickly and without intentionally launching subagents during the measurement window.
Environment
26.721.41059(build5848)0.146.0-alpha.3.126.3(Darwin 25.3.0 arm64)Mac14,2, Apple M2, 16 GB RAMObserved after about 40 minutes from a full app restart
macOS displayed the Application Memory Is Low dialog and attributed:
11.69 GBto ChatGPT11.68 GBto Google ChromeRead-only process accounting showed why a simple executable-path RSS sum under-reports the impact:
ChatGPT.app:30, combined RSS1.64 GiB157processes, combined RSS5.01 GiBfootprintfor the 30 app-path processes:3730 MB, including2244 MBswapped/compressed6.3 GB3.64 GB / 5 GBThe ChatGPT descendant tree included repeated tool stacks:
47chrome-devtools-MCP-related launcher/server/watchdog processes26xcodebuildmcp-related processes26memory-MCP-related processes26GitHub-MCP-related processes12Computer Usenode_replprocessesThe core ChatGPT/Codex processes themselves were not enough to explain the macOS number. The large discrepancy came from tool/MCP/browser descendants plus compressed and swapped memory being attributed to the application group.
User-impact comparison
As direct product feedback: Claude Code handles this lifecycle far better in comparable daily use on the same machine. Its helper-process and memory footprint stays bounded instead of retaining a new full tool stack for multiple tasks until macOS raises a system-wide memory warning. Codex is materially behind Claude Code in this area today.
This makes Codex Desktop unreliable on a normal 16 GB Mac even shortly after restart. Restarting only resets the symptom; it does not solve the lifecycle problem.
Expected behavior
Please prioritize this as a desktop reliability issue rather than only a performance optimization.