Skip to content

[macOS] Codex Desktop creates hidden threads and leaks one MCP process pool approximately every 5 minutes while idle #43971

Open

Description

@chiuser

What version of the Codex App are you using (From “About Codex” dialog)?

Codex Desktop: 26.901.51231

What subscription do you have?

pro 20x

What platform is your computer?

MacOS

What issue are you seeing?

After a clean restart, Codex Desktop repeatedly creates a new hidden thread while the application is idle. Each hidden thread starts a complete MCP process pool, including XcodeBuildMCP, Context7, Computer Use, node_repl, and app-tool servers. The previous pool is not terminated.

The behavior is unbounded: process count and memory usage grow continuously until the application is fully quit.

What steps can reproduce the bug?

Steps to reproduce

  1. Start Codex Desktop after fully quitting it.
  2. Open or resume one local Codex task.
  3. Leave the application open and do not interact with it.
  4. Periodically inspect the app log and the descendants of the Codex app-server process.
  5. Observe that a new hidden thread/start occurs approximately every 5 minutes 20 seconds and starts another complete MCP pool.

No scheduled automation or user action is required to trigger the growth.

What is the expected behavior?

  • An idle application should not continuously create new threads.
  • If an internal draft, prewarm, or code-mode thread is periodically replaced, its MCP child processes should be shut down.
  • MCP process counts and memory usage should stabilize while the application is idle.

Additional information

[macOS] Codex Desktop creates hidden threads and leaks one MCP process pool approximately every 5 minutes while idle

Summary

After a clean restart, Codex Desktop repeatedly creates a new hidden thread while the application is idle. Each hidden thread starts a complete MCP process pool, including XcodeBuildMCP, Context7, Computer Use, node_repl, and app-tool servers. The previous pool is not terminated.

The behavior is unbounded: process count and memory usage grow continuously until the application is fully quit.

Environment

  • macOS 26.6.2 (25G83), Apple Silicon (arm64)
  • Physical memory: 36 GiB
  • Codex Desktop: 26.901.51231
  • Bundled Codex CLI / app-server: 0.153.4
  • One visible Codex task was open
  • No scheduled Codex automations were configured
  • Global openclaw MCP: disabled
  • Global XcodeBuildMCP: disabled
  • context7: enabled
  • The build-ios-apps plugin supplied the runtime xcodebuildmcp server

Steps to reproduce

  1. Start Codex Desktop after fully quitting it.
  2. Open or resume one local Codex task.
  3. Leave the application open and do not interact with it.
  4. Periodically inspect the app log and the descendants of the Codex app-server process.
  5. Observe that a new hidden thread/start occurs approximately every 5 minutes 20 seconds and starts another complete MCP pool.

No scheduled automation or user action is required to trigger the growth.

Expected behavior

  • An idle application should not continuously create new threads.
  • If an internal draft, prewarm, or code-mode thread is periodically replaced, its MCP child processes should be shut down.
  • MCP process counts and memory usage should stabilize while the application is idle.

Actual behavior

  • The renderer sends thread/start with conversationId=null approximately every 5 minutes 20 seconds.
  • Each new thread starts the same MCP servers.
  • These thread IDs do not have corresponding session files and do not appear as user-visible tasks.
  • No MCP stop, shutdown, reap, or thread-close events were found in the app log.
  • Every recorded XcodeBuildMCP startup still had a corresponding live server process several hours later.

Observed timeline

All timestamps below are UTC:

21:51:20  hidden thread/start; MCP pool started
21:56:22  hidden thread/start; another MCP pool started
22:01:36  hidden thread/start; another MCP pool started
22:06:54  hidden thread/start; another MCP pool started
...
01:24:59  hidden thread/start; another MCP pool started
01:30:23  hidden thread/start; another MCP pool started

During the idle interval, the number of live XcodeBuildMCP server instances increased from 3 to approximately 43. No matching shutdown events occurred.

Sanitized log excerpt

2026-09-09T01:24:59.190Z ... conversationId=null ... method=thread/start originWebcontentsId=1
2026-09-09T01:24:59.192Z ... server=context7 status=starting threadId=<ephemeral-thread-A>
2026-09-09T01:24:59.192Z ... server=node_repl status=starting threadId=<ephemeral-thread-A>
2026-09-09T01:24:59.192Z ... server=cua_repl status=starting threadId=<ephemeral-thread-A>
2026-09-09T01:24:59.192Z ... server=xcodebuildmcp status=starting threadId=<ephemeral-thread-A>

2026-09-09T01:30:23.852Z ... conversationId=null ... method=thread/start originWebcontentsId=1
2026-09-09T01:30:23.852Z ... server=xcodebuildmcp status=starting threadId=<ephemeral-thread-B>
2026-09-09T01:30:23.854Z ... server=node_repl status=starting threadId=<ephemeral-thread-B>
2026-09-09T01:30:23.854Z ... server=context7 status=starting threadId=<ephemeral-thread-B>

Process snapshot after several hours

After returning to the app and collecting diagnostics, the process table contained:

Process group Live processes Summed RSS
XcodeBuildMCP 48 npm launchers + 48 Node servers about 5.46 GiB
Context7 49 npm launchers + 49 Node servers about 4.86 GiB
node_repl 96 about 1.16 GiB
Computer Use launchers 47 about 1.76 GiB
App-tool server.mjs processes 47 about 1.88 GiB
All app-server descendants 538 about 20.8 GiB

The summed RSS values are not equivalent to unique physical memory because shared pages may be counted more than once. The process-count growth and lack of cleanup are independent of that limitation.

Configuration control results

  • Disabling global OpenClaw worked: no openclaw mcp serve processes were created after restart.
  • Disabling global XcodeBuildMCP removed the duplicate global definition.
  • The lower-case xcodebuildmcp supplied by the build-ios-apps plugin still started once for every hidden thread.
  • Therefore, individual MCP configuration changes reduce the memory cost per leaked thread but do not stop the hidden-thread creation loop.

Impact

  • Continuously increasing process count and memory use while the app is idle
  • Heavy swap or memory pressure on long-running sessions
  • Hundreds of orphaned MCP-related descendants after a few hours
  • Full application restart required to recover resources

Workaround

  • Fully quit and reopen Codex Desktop to reclaim the leaked process pools.
  • Disabling heavy MCP servers or plugins reduces the growth rate, but it does not fix the lifecycle issue.

Suspected area

The evidence points to a renderer-initiated draft/prewarm or code-mode-host lifecycle path: a new internal thread is periodically created, but the previous thread and its MCP process pool are not disposed. This is an inference from the log sequence, not a confirmed implementation-level root cause.

Additional evidence available

Raw app logs are available locally, but they are not attached because they may contain task names, local paths, prompts, or other sensitive information. A further sanitized excerpt can be provided if needed.

Activity

  1. added
    appIssues related to the Codex desktop app
    on Sep 9, 2026
  2. added
    bugSomething isn't working
    mcpIssues related to the use of model context protocol (MCP) servers
    app-serverIssues involving app server protocol or interfaces
    on Sep 9, 2026
  3. argszero commented on Sep 9, 2026

    @argszero

    This looks like the same underlying "idle app-server spawns/leaks MCP process pools" family as #43471 (ChatGPT 26.901 / codex-cli 0.153.4: idle app-server leaks SkyComputerUseClient mcp helpers — pair per 5 min → 6.4 GB RSS + kernel panics). Your macOS report adds a fresh angle I hadn't seen on the family — the hidden threads that each spawn their own full job pool (XcodeBuildMCP, Context7, Computer Use, node_repl, app-tool servers) with the prior pool never torn down.

    #43471 tracks the reconnect/keepalive spawn path in codex-rs (and links the turn-ended variant #26293/#29157). If the idle timer is creating a new hidden thread (rather than just reconnecting the same one), the unbounded growth you describe would be the pool-per-thread accumulation on top of the pair-per-5-min keepalive leak — worth confirming whether it's one new hidden thread each cycle (pool-per-thread) or the same thread reconnecting (pair-per-cycle). Either way this is the same class and I'd suggest coalescing the repro + memory/proc observations there so maintainers see the combined picture.

  4. chiuser commented on Sep 9, 2026

    @chiuser
    Author

    Confirmed: this is one new hidden thread per cycle, not the same thread reconnecting.

    Verification summary

    • Idle observation window: 2026-09-08T21:51:20Z through 2026-09-09T01:30:23Z.
    • The app log contained 42 renderer-initiated method=thread/start events with conversationId=null.
    • Correlating each thread/start with the immediately following server=xcodebuildmcp status=starting event produced 42 distinct hidden thread IDs.
    • The one visible task was restored separately with method=thread/resume, not thread/start.
    • Including that visible task, the log contained 43 XcodeBuildMCP starts associated with 43 distinct thread IDs by the end of the idle window.
    • There were 0 MCP stop/shutdown/reap events and 0 thread-close events in the same app session.
    • No Codex scheduled automations were configured.
    • After later diagnostic activity, the cumulative count reached 48 XcodeBuildMCP starts with 48 distinct thread IDs. The process table contained exactly 48 live npm launchers and 48 live XcodeBuildMCP Node servers, confirming that the earlier pools had not been reclaimed.

    The word “inference” in my original report referred only to whether the responsible implementation path is draft/prewarm or code-mode-host. The creation of a distinct hidden thread on each cycle is directly confirmed by the logs.

    Sanitized log excerpt

    Real thread IDs and request IDs have been replaced with stable placeholders. Usernames, local paths, task names, project data, and prompt content are omitted.

    2026-09-09T01:24:59.190Z [AppServerConnection] response_routed conversationId=null method=thread/start originWebcontentsId=1
    2026-09-09T01:24:59.192Z [AppServerConnection] mcp_server_startup_status_updated server=context7 status=starting threadId=<hidden-thread-H041>
    2026-09-09T01:24:59.192Z [AppServerConnection] mcp_server_startup_status_updated server=node_repl status=starting threadId=<hidden-thread-H041>
    2026-09-09T01:24:59.192Z [AppServerConnection] mcp_server_startup_status_updated server=cua_repl status=starting threadId=<hidden-thread-H041>
    2026-09-09T01:24:59.192Z [AppServerConnection] mcp_server_startup_status_updated server=codex_apps status=starting threadId=<hidden-thread-H041>
    2026-09-09T01:24:59.192Z [AppServerConnection] mcp_server_startup_status_updated server=codegraph status=starting threadId=<hidden-thread-H041>
    2026-09-09T01:24:59.192Z [AppServerConnection] mcp_server_startup_status_updated server=xcodebuildmcp status=starting threadId=<hidden-thread-H041>
    2026-09-09T01:24:59.192Z [AppServerConnection] mcp_server_startup_status_updated server=codex_app status=starting threadId=<hidden-thread-H041>
    
    2026-09-09T01:30:23.852Z [AppServerConnection] response_routed conversationId=null method=thread/start originWebcontentsId=1
    2026-09-09T01:30:23.852Z [AppServerConnection] mcp_server_startup_status_updated server=xcodebuildmcp status=starting threadId=<hidden-thread-H042>
    2026-09-09T01:30:23.853Z [AppServerConnection] mcp_server_startup_status_updated server=cua_repl status=starting threadId=<hidden-thread-H042>
    2026-09-09T01:30:23.854Z [AppServerConnection] mcp_server_startup_status_updated server=codegraph status=starting threadId=<hidden-thread-H042>
    2026-09-09T01:30:23.854Z [AppServerConnection] mcp_server_startup_status_updated server=node_repl status=starting threadId=<hidden-thread-H042>
    2026-09-09T01:30:23.854Z [AppServerConnection] mcp_server_startup_status_updated server=codex_app status=starting threadId=<hidden-thread-H042>
    2026-09-09T01:30:23.854Z [AppServerConnection] mcp_server_startup_status_updated server=context7 status=starting threadId=<hidden-thread-H042>
    

    The different placeholders represent different original thread IDs; they are not repeated or reconnected IDs.

    Sanitized mapping of all 42 hidden-thread cycles
    H001  2026-09-08T21:51:20.759Z
    H002  2026-09-08T21:56:22.344Z
    H003  2026-09-08T22:01:36.726Z
    H004  2026-09-08T22:06:54.526Z
    H005  2026-09-08T22:12:11.223Z
    H006  2026-09-08T22:17:30.918Z
    H007  2026-09-08T22:22:51.557Z
    H008  2026-09-08T22:28:09.446Z
    H009  2026-09-08T22:33:29.247Z
    H010  2026-09-08T22:38:58.793Z
    H011  2026-09-08T22:44:20.274Z
    H012  2026-09-08T22:49:41.310Z
    H013  2026-09-08T22:55:01.233Z
    H014  2026-09-08T23:00:20.969Z
    H015  2026-09-08T23:05:50.207Z
    H016  2026-09-08T23:11:10.372Z
    H017  2026-09-08T23:16:29.625Z
    H018  2026-09-08T23:21:50.962Z
    H019  2026-09-08T23:27:12.163Z
    H020  2026-09-08T23:32:32.689Z
    H021  2026-09-08T23:37:52.957Z
    H022  2026-09-08T23:43:13.688Z
    H023  2026-09-08T23:48:35.187Z
    H024  2026-09-08T23:53:55.298Z
    H025  2026-09-08T23:59:14.065Z
    H026  2026-09-09T00:04:31.949Z
    H027  2026-09-09T00:09:57.883Z
    H028  2026-09-09T00:15:18.204Z
    H029  2026-09-09T00:20:40.746Z
    H030  2026-09-09T00:26:00.553Z
    H031  2026-09-09T00:31:21.644Z
    H032  2026-09-09T00:36:43.881Z
    H033  2026-09-09T00:42:03.423Z
    H034  2026-09-09T00:47:25.024Z
    H035  2026-09-09T00:52:47.991Z
    H036  2026-09-09T00:58:09.463Z
    H037  2026-09-09T01:03:30.926Z
    H038  2026-09-09T01:08:54.426Z
    H039  2026-09-09T01:14:16.790Z
    H040  2026-09-09T01:19:38.416Z
    H041  2026-09-09T01:24:59.190Z
    H042  2026-09-09T01:30:23.852Z
    

    Each Hnnn maps to a different original thread ID observed on the corresponding XcodeBuildMCP startup event.

    This confirms the pool-per-new-thread accumulation variant. It is not merely a pair-per-reconnect leak on one stable thread.

    This looks like the same underlying "idle app-server spawns/leaks MCP process pools" family as #43471 (ChatGPT 26.901 / codex-cli 0.153.4: idle app-server leaks SkyComputerUseClient mcp helpers — pair per 5 min → 6.4 GB RSS + kernel panics). Your macOS report adds a fresh angle I hadn't seen on the family — the hidden threads that each spawn their own full job pool (XcodeBuildMCP, Context7, Computer Use, node_repl, app-tool servers) with the prior pool never torn down.

    #43471 tracks the reconnect/keepalive spawn path in codex-rs (and links the turn-ended variant #26293/#29157). If the idle timer is creating a new hidden thread (rather than just reconnecting the same one), the unbounded growth you describe would be the pool-per-thread accumulation on top of the pair-per-5-min keepalive leak — worth confirming whether it's one new hidden thread each cycle (pool-per-thread) or the same thread reconnecting (pair-per-cycle). Either way this is the same class and I'd suggest coalescing the repro + memory/proc observations there so maintainers see the combined picture.

  5. jorgeschnura commented on Sep 15, 2026

    @jorgeschnura

    Windows reproduction, plus the prewarm mechanism in the desktop bundle and evidence that discarded prewarmed threads leak their MCP children

    Same family as #44525 and #30408. Adding a Windows data point and, more usefully, where the periodic hidden thread/start comes from in the Electron app, plus what I can and cannot tell about why the app-server never reaps those pools.

    Environment

    • Windows 11 Home 26200, x64, 32 GB RAM
    • Codex Desktop 26.908.4834.0 (app-server sees client_version="26.908.40834"), bundled codex 0.154.0
    • MCP config: 11 HTTP servers (no child processes) plus the bundled stdio ones (node_repl, cua_repl, codex-security, codex-app-tools, artifact-template-picker) and one personal plugin providing a stdio server (a PowerShell launcher that spawns a node child; both stay alive). No automations were running (two exist, both PAUSED, automation_runs empty), so they are not the source.

    Observation

    The app-server started at 23:56. At 08:47 it had 156 direct children: 30 PowerShell launchers from my plugin, 34 node, 30 node_repl.exe, 60 cmd.exe (two per pool), one codex-code-mode-host.exe, one conhost: one full set per spawn. From 00:12 to 03:52, with the window visible, no user activity and zero turns (the account had no usage credits left), a complete stdio MCP pool (five process groups, six when artifact-template-picker is included) was spawned every 10 minutes at xx:x2:11, and none were ever reaped. Each pool correlates with one thread/start from client_name="Codex Desktop" carrying a new thread id. In ~/.codex/logs_2.sqlite the only row most of those threads ever get is the WARN codex_core::shell_snapshot line emitted during session_init (thread_spawn ... session_init ... shell_snapshot{thread_id=...}); for those hours the log has no INFO/DEBUG/TRACE rows at all, so that WARN is the only trace of them. None of those thread ids ever get a rollout under sessions/; the ones I spot-checked also have no thread-writer-locks/<id>.lock and no row in state_5.sqlite (a real thread has all three).

    It is not idle-only: over the previous working day 61 of 68 thread/start requests were hidden threads that never received a turn, so the pools also pile up while the app is in use. After ~9.5 hours: ~5.5 GB working set in my plugin's launcher+node pairs alone, ~11 GB counting the bundled servers, machine at 84 % RAM.

    The cadence stopped at 03:52 and did not simply resume in the morning: the app woke up at 07:35 (plugin bundle sync, an mcpServerStatus/list from the Desktop connection, a thread/resume from the Android client, which respawned MCP servers without any session_init), the next hidden thread/start came at 08:00, and the child count then stayed flat until at least 09:30. The overnight stop is consistent with a renderer timer that pauses while the window is occluded, but I have not confirmed that, nor why the idle cadence is 10 minutes here versus the ~5 min 20 s reported above.

    Mechanism (from resources/app.asar, Electron main bundle .vite/build/main-*.js)

    • The prewarmed-thread manager (this.prewarmedThreadManager, class minified as Yxe) keeps one prewarmed thread per cwd (prewarmedThreadByCwd). prewarmConversation sends thread/start via prewarmThreadStart ahead of use so that starting a conversation is instant; the renderer asks for it through the thread-prewarm-start IPC message.
    • Freshness: Jxe = 4.75 * 60 seconds; isFresh(createdAtSeconds) is now/1e3 - createdAt < Jxe.
    • hasPrewarmedThread discards a stale entry (or one whose workspaceRoots, serviceName or requestedHistoryMode no longer match) with discardPrewarmedThread(e) → deletePrewarmedThread(threadId) → sendRequest('thread/delete', {threadId}), and a new prewarm thread/start follows. setPrewarmedThreadPromise also discards the previous entry when a new one is set.
    • That 4.75-minute freshness plus whatever tick re-evaluates it would be consistent with the "~5 min 20 s" cadence reported in this issue.

    What I can and cannot tell about the leak

    The app-server does know how to tear MCP children down: a real thread that was closed (op: Shutdown → Shutting down Codex instance / clearing thread listener during thread-state teardown) left no child processes, and a session init that got cancelled during startup cleaned up too (rmcp::service logs task cancelled / serve finished quit_reason=Cancelled). A prewarmed thread that never had a turn and gets discarded never goes through that path: its MCP children stay alive indefinitely (about ten hours in my case, until I killed them). Whether thread/delete is not sent for them or is handled without shutting down the session-owned MCP runtime I cannot tell from my logs (only 10 of my 170 thread/start requests even have a TRACE row and no thread/delete does); if there is a log filter that captures every app-server request I will rerun and confirm.

    Killing the orphaned plugin pairs externally (26 of them, at 09:51) was handled cleanly: the app-server logged input stream terminated / serve finished quit_reason=Closed at INFO for each one, no WARN/ERROR, no respawn, and the live threads kept their own MCP processes.

    Suggested fix

    • On discard/thread/delete of a thread that never had a turn, run the same shutdown a closed thread gets (op: Shutdown → Shutting down Codex instance, which does take the MCP children down), or
    • Do not start MCP servers for prewarmed threads until the first turn actually needs them.

    Workaround on Windows

    A scheduled task that kills MCP children whose session_init thread never got a rollout or lock, is older than an hour and is not the newest prewarmed one keeps this in check (children of one init are born in the same second or the next, so they can be attributed by creation time); the app-server has been unaffected so far. Happy to share it, or targeted log excerpts, if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingmcpIssues related to the use of model context protocol (MCP) serversperformance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions