MCP servers eat up memory when multi-tasking in Codex App

Open 💬 15 comments Opened Feb 10, 2026 by yahelroro0196
💡 Likely answer: A maintainer (guidedways, contributor) responded on this thread — see the highlighted reply below.

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:

  1. 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
  2. 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
  3. 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.

View original on GitHub ↗

15 Comments

guidedways contributor · 6 months ago

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.

yahelroro0196 · 6 months ago

@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.

francoisrob · 6 months ago

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

yahelroro0196 · 6 months ago

bump? :)

ginishuh · 6 months ago

Related issues that may help narrow root cause and mitigation:

  • #12333: Codex CLI multi-agent spawns duplicate Serena/stdIO MCP instances instead of sharing one connection.
  • #12335: Feature request for automatic lifecycle cleanup (auto-close completed/idle sub-agents + teardown unused MCP clients).

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.

ribber · 5 months ago

I encountered the same problem.

RemarkRemedy · 5 months ago

I dug into this locally and can confirm a more specific app-side failure mode that matches the original report.

My setup:

  • Codex App 26.311.21342 (build 993)
  • macOS Darwin 25.3.0 arm64 arm
  • stdio MCP servers configured for omx_state, omx_memory, omx_code_intel, omx_trace, and a custom claude_plan_review

What I observed:

  • The long-lived parent is Codex.app/Contents/Resources/codex app-server
  • Each loaded conversation spawns a fresh MCP bundle under that app-server
  • After the conversation ends or is no longer viewed, those MCP children remain alive with the same live PPID as the app-server
  • They are not orphaned to launchd; they stay attached to the still-running app-server and accumulate over time

A representative process snapshot looked like this:

63145 /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled
├── 10483 node .../oh-my-codex/dist/mcp/trace-server.js
├── 10484 node .../oh-my-codex/dist/mcp/state-server.js
├── 10488 node .../oh-my-codex/dist/mcp/memory-server.js
├── 10489 node .../oh-my-codex/dist/mcp/code-intel-server.js
├── 10481 node .../claude-plan-review-mcp.mjs
├── 10618 node .../oh-my-codex/dist/mcp/trace-server.js
├── 10624 node .../oh-my-codex/dist/mcp/state-server.js
├── 10625 node .../oh-my-codex/dist/mcp/memory-server.js
├── 10626 node .../oh-my-codex/dist/mcp/code-intel-server.js
└── 10621 node .../claude-plan-review-mcp.mjs

Source-level findings from openai/codex:

  • MCP servers are created per thread / conversation, not globally. ThreadManager::spawn_thread_with_source() goes through Codex::spawn(), which creates a fresh McpConnectionManager for that session.
  • The rmcp client already has proper child cleanup if the manager is actually dropped: stdio clients use kill_on_drop(true) and a process-group guard that terminates / kills the process group on drop.
  • The app-server's intended unload path is thread/unsubscribe. If the last subscriber unsubscribes, it shuts down the thread and emits thread/closed.
  • However, on connection removal the app-server currently retains zero-subscriber threads instead of unloading them. The relevant log message in the current source is:

retaining thread listener after connection disconnect left zero subscribers

  • The core session shutdown path also does not explicitly tear down the MCP manager.

Important control experiment:

  • I launched the same Node MCP servers manually with stdin connected to /dev/null
  • Both the oh-my-codex server and the custom plan-review server exited normally
  • So this does not look like an MCP server bug first; it looks like Codex is keeping the thread and its stdio pipes alive

My current read is:

  1. the desktop app is not unloading old conversations reliably
  2. because those thread objects stay loaded in the app-server, their per-conversation McpConnectionManagers never drop
  3. therefore the MCP child processes stay alive indefinitely

This seems closely related to #12491, but the narrower root cause here is conversation / thread unload lifecycle in the desktop app, especially around thread/unsubscribe and disconnect handling.

dorukardahan · 5 months ago

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

  • Codex.app 26.311.21342 (build 993)
  • macOS Darwin 25.3.0, Apple Silicon
  • about 18 conversations over ~2.5 days
  • MCPs: context7, playwright, chrome-devtools, webflow mcp-remote, custom Python MCP

Before I quit the app

Node: 100
Python (B12): 22
Total visible MCP-side load: ~124 processes

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 #12333 and #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:

Node: 100 -> 10
Python (B12): 22 -> 4
About 90 Codex child processes died on quit

But cleanup was still not complete. I still had:

2x orphaned `chrome_crashpad_handler`
2x orphaned `playwright-mcp`
1x orphaned `twitterapi` MCP

The old survivors matched earlier Codex sessions from March 9, 2026 and March 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:

  • Claude Code via Superset.app had a normal small active MCP set
  • the buildup and leftovers were from Codex.app

#10710 is 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.

Keesan12 · 2 months ago

One architecture split that has helped us on a similar runtime: treat MCP child lifetime as a separate admission problem from thread lifetime.

Concretely:

  • keep per-server ownership metadata (owner_threads, last tool call timestamp, rss, idle_since)
  • tear down on refcount == 0 plus a short idle TTL, instead of tying the child to whether the parent app-server is still alive
  • expose a tiny operator receipt for every surviving helper: why it is still admitted, who still owns it, and what event will release it

That tends to solve two different pain points at once:

  1. real leaks become obvious because the helper has no live owner / no recent activity
  2. legitimate long-lived helpers stop looking like leaks because the runtime can explain why they are still around

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_alive receipt 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.

iddictive · 1 month ago

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:

  1. Drop and reap every task-owned stdio MCP process group when a task is archived, closed, cancelled, or otherwise becomes inactive.
  2. Pool stateless/shareable MCP clients at the app-server or project level instead of spawning a full stack per retained task.
  3. Keep stateful/browser/account-scoped MCPs session-scoped, with an explicit idle timeout and cleanup contract.
  4. Start MCP servers on first tool use rather than eagerly for every task.
  5. Add a per-server process/RSS budget and surface ownership in the MCP UI: server, transport, owning tasks, process count, RSS, and a safe “restart idle instances” action.
  6. Prefer native Streamable HTTP when a provider supports it; warn when a stdio mcp-remote-style bridge duplicates a transport Codex can already speak directly.
  7. Add a regression test that opens and retires multiple tasks, then verifies bounded process count and complete child-process cleanup after the lifecycle boundary.

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.

YouAreOK · 1 month ago

I can reproduce this MCP lifecycle/resource retention issue on Codex Desktop for Windows.

Environment

  • Codex Desktop: 26.715.8383.0
  • Installation: Microsoft Store / MSIX
  • OS: Windows 11 Enterprise LTSC, build 26100, x64
  • Device: Lenovo 21TK
  • CPU: Intel Core Ultra 7 255H, 16 logical processors
  • RAM: 32 GB
  • MCP servers include Playwright MCP, Playwright test MCP, simple-yapi-mcp, bundled MCP servers, and node_repl.

Reproduction steps

  1. Start Codex Desktop.
  2. Open or resume a thread.
  3. Allow the turn to complete.
  4. Switch to another thread and resume it.
  5. Repeat this with several threads.
  6. Leave the completed/inactive threads idle.
  7. Inspect node_repl, node, cmd, and volta processes in Task Manager or PowerShell.

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.exe app-server retained five separate node_repl.exe direct child processes, started at different times:

  • 10:33:46
  • 10:34:04
  • 12:37:06
  • 12:46:03
  • 12:49:37

The resulting process counts were:

  • 1 × codex.exe
  • 5 × node_repl.exe
  • 40 × node.exe
  • 51 × cmd.exe
  • 15 × volta.exe

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:

  • the previous thread changing to active=false
  • the next thread changing to active=true
  • thread/resume
  • disposeAfterSessionActivity=false

In the inspected log:

  • disposeAfterSessionActivity=false: 37 occurrences
  • disposeAfterSessionActivity=true: 0 occurrences
  • MCP/node_repl shutdown, terminate, or dispose messages: 0 occurrences

Fully 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:

  1. MCP servers for completed/inactive threads should be terminated after an idle timeout.
  2. Finished or archived threads should explicitly dispose their MCP connections and child process trees.
  3. Codex should enforce a maximum number of retained inactive runtimes using LRU eviction.
  4. MCP servers should start lazily only when their tools are used.
  5. A user-facing command should allow restarting or disposing MCP servers for an inactive thread.

This appears related to #11324 and #21984, but this reproduction is specifically from Codex Desktop on Windows/MSIX.

<!-- Failed to upload "image.png" -->

freaksdotcom · 1 month ago

I ran a controlled /exit vs /archive comparison and can reproduce a narrower lifecycle boundary on Linux.

Environment

  • Codex CLI/app-server: 0.145.0
  • Ubuntu 26.04, x86_64
  • Kernel: Linux 7.0.0-28-generic
  • Long-lived shared app server: codex ... app-server --listen unix://
  • stdio MCPs in each bundle: Probe, Vera, and Mempalace

Method

For each trial I:

  1. captured the app server's direct MCP child PIDs
  2. opened a new TUI against the existing Unix-socket app server
  3. exercised the configured MCPs
  4. captured the newly created PIDs while the test TUI was open
  5. used either /exit or /archive
  6. checked those exact PIDs after 5 and 30 seconds

This tracks PID identity rather than relying on aggregate process counts, since other Codex clients were active concurrently.

Results

/exit

  • The test workload created two complete per-thread MCP bundles (6 new PIDs total; the turn exercised agent activity).
  • All 6 remained alive after 5 seconds.
  • All 6 remained alive after 30 seconds and were still alive several minutes later.
  • Every survivor still had the live app-server process as its PPID.
  • The TUI connection closed, the app-server client count returned to its prior level, and the log recorded thread/unsubscribe for that connection.

/archive control

  • A fresh test thread created one complete MCP bundle (3 new PIDs).
  • 0 of 3 survived after 5 seconds.
  • 0 of 3 survived after 30 seconds.
  • Logs showed thread/archive, followed by clearing 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 /archive deterministically 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.

freaksdotcom · 1 month ago

Follow-up to my earlier /exit vs /archive reproduction: 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. /exit removes 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 Auto attachments from retaining Explicit owners 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, and git diff --check are 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 load libssl.so.3.

hrkzogw · 1 month ago

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

  • ChatGPT app 26.721.41059 (build 5848)
  • Bundled Codex 0.146.0-alpha.3.1
  • macOS Darwin 25.5.0, Apple Silicon, 32 GB RAM
  • 6 enabled stdio MCP servers (brave-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_task at 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:

  • 63 thread IDs present.
  • Zero thread/closed events 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.
  • Zero records containing rpc.method="thread/unsubscribe".

So threads stayed loaded with 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/start auto-subscribes the client, turn/completed does 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=1 and zero children for another ~24 hours, while a new app-server ran concurrently. The two competed and produced repeated remote-control 409s 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

  1. Send thread/unsubscribe when 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 string thread/unsubscribe is present in the bundle; the call condition is what I could not determine.
  2. Treat a subscriber as a lease with a heartbeat or expiry, rather than an indefinite hold.
  3. Cap the number of loaded threads (LRU), independent of subscriber state.
  4. Start MCP servers lazily on first tool use instead of eagerly at session init. Threads that never call a tool would then cost nothing.
  5. Expose loaded-thread count, subscriber count, and MCP PID/PGID with owner thread ID through a diagnostic endpoint.

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 out codex-code-mode-host and 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_tools do not help — the server still starts at session init; only enabled = false on the server itself prevents the process.

muke1838-cloud · 1 month ago

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

  • Codex App 26.721.41059 (build 5848)
  • Bundled Codex CLI 0.146.0-alpha.3.1
  • ChatGPT Pro
  • macOS 26.3 (Darwin 25.3.0 arm64)
  • MacBook Mac14,2, Apple M2, 16 GB RAM

Observed after about 40 minutes from a full app restart

macOS displayed the Application Memory Is Low dialog and attributed:

  • 11.69 GB to ChatGPT
  • 11.68 GB to Google Chrome

Read-only process accounting showed why a simple executable-path RSS sum under-reports the impact:

  • Processes whose executable path is inside ChatGPT.app: 30, combined RSS 1.64 GiB
  • Full descendant tree rooted at the ChatGPT main process: 157 processes, combined RSS 5.01 GiB
  • footprint for the 30 app-path processes: 3730 MB, including 2244 MB swapped/compressed
  • System compressor: about 6.3 GB
  • Swap in use: 3.64 GB / 5 GB
  • macOS began killing idle low-priority processes under memory pressure

The ChatGPT descendant tree included repeated tool stacks:

  • 47 chrome-devtools-MCP-related launcher/server/watchdog processes
  • 26 xcodebuildmcp-related processes
  • 26 memory-MCP-related processes
  • 26 GitHub-MCP-related processes
  • 12 Computer Use node_repl processes

The 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

  • MCP servers should be shared, lazy-started, or torn down when their owning task/thread is no longer active.
  • A task that never calls an MCP tool should not retain a complete global MCP process bundle.
  • Tool processes should have an observable owner thread/task ID so stale groups can be diagnosed and safely reclaimed.
  • Total helper count and memory should remain bounded across task switches and reconnects.

Please prioritize this as a desktop reliability issue rather than only a performance optimization.