[Windows Desktop 26.810.7004] Thread-scoped STDIO MCP processes and local-proxy CLOSE_WAIT sockets accumulate

Open 💬 3 comments Opened Aug 17, 2026 by tvivt
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of the Codex App are you using?

ChatGPT/Codex Desktop 26.810.7004.0 (x64)

Platform

Windows 11 Pro, version 10.0.26200 (build 26200)

Summary

During normal use of Codex Desktop with several global local STDIO MCP servers, two forms of resource accumulation occur in the same app-server lifetime:

  1. A new set of local STDIO MCP process trees is started for many threads/tasks and old sets remain alive.
  2. The Codex app-server accumulates both established and CLOSE_WAIT TCP sockets to a local HTTP proxy.

The socket owner is the Codex app-server itself, not any individual MCP server. The two symptoms are correlated in the same long-lived Desktop session, but I have not established that MCP initialization causes the socket accumulation.

This report adds current Windows Desktop 26.810.7004.0 evidence and local-proxy socket ownership to the existing MCP lifecycle reports.

Configuration

Representative global local STDIO MCP servers:

  • CodeGraph: codegraph serve --mcp
  • Semble: semble.exe (stdio MCP mode)
  • FastCtx: fastctx.exe serve
  • Codex node_repl

HTTPS traffic is routed through a local proxy at 127.0.0.1:10888. The specific port should not be material; it is included to make the socket evidence reproducible.

Observed behavior

After continued normal use in one Desktop app-server lifetime:

| Direct child/runtime | Count |
|---|---:|
| CodeGraph launchers | 32 |
| Semble launchers | 32 |
| FastCtx servers | 31 |
| node_repl | 32 |

Including launcher and backend descendants:

| Process family | Processes | Approx. working set |
|---|---:|---:|
| CodeGraph-related | 117 | 5,277 MB |
| Semble-related | 96 | 5,000 MB |
| FastCtx-related | 31 | 1,317 MB |
| Other app-server descendants | 49 | 1,265 MB |
| Codex app-server | 1 | 442 MB |

At the same time, the Codex app-server owned these connections to the local proxy:

| State | Count |
|---|---:|
| ESTABLISHED | 36 |
| CLOSE_WAIT | 39 |

An earlier sample from the previous app-server lifetime showed approximately 39 repeated MCP runtime sets and 46 established plus 51 CLOSE_WAIT connections to the same local proxy.

Fully restarting Codex removes the old process tree and sockets, but both counts grow again as threads are opened, resumed, or used.

Network ownership checks showed:

  • Connections to 127.0.0.1:10888 were owned by codex.exe app-server.
  • Semble held only loopback connections used by its own local process hierarchy.
  • CodeGraph and FastCtx did not own the accumulated proxy connections.

Steps to reproduce

  1. On Windows, configure multiple global local STDIO MCP servers.
  2. Configure Codex traffic to use a local HTTP proxy.
  3. Fully start Codex Desktop.
  4. Open, resume, or switch among multiple local tasks over time.
  5. Let completed or idle tasks remain in the task list.
  6. Inspect direct and indirect descendants of the Desktop app-server.
  7. Inspect TCP state for the app-server PID.

Example PowerShell checks:

$appServer = Get-CimInstance Win32_Process |
  Where-Object {
    $_.Name -eq 'codex.exe' -and
    $_.CommandLine -match 'app-server'
  } |
  Select-Object -First 1

Get-CimInstance Win32_Process |
  Where-Object ParentProcessId -eq $appServer.ProcessId |
  Group-Object CommandLine |
  Sort-Object Count -Descending |
  Select-Object Count, Name

Get-NetTCPConnection -OwningProcess $appServer.ProcessId |
  Where-Object {
    $_.RemoteAddress -eq '127.0.0.1' -and
    $_.RemotePort -eq 10888
  } |
  Group-Object State |
  Select-Object Count, Name

Expected behavior

  • MCP subprocess trees belonging to completed, closed, unloaded, or expired thread runtimes should be terminated.
  • Alternatively, compatible local MCP servers should be shared or pooled with a bounded lifetime across threads.
  • Half-closed proxy sockets should be closed promptly by the app-server instead of accumulating in CLOSE_WAIT.
  • Long-running Desktop use should have bounded process, memory, and socket counts.

Actual behavior

  • Each new/resumed thread appears to initialize another full set of local STDIO MCP servers.
  • Many old MCP process groups remain attached to the app-server.
  • Memory consumption grows into multiple gigabytes.
  • The app-server accumulates dozens of half-closed proxy sockets.
  • Restarting Codex is the practical temporary cleanup.

Suggested areas to investigate

  • Thread/session MCP manager disposal after completion, archive, unload, or idle timeout.
  • A supported shared / reuse / idle_timeout mode for compatible local MCP servers.
  • App-server HTTP connection-pool handling when a proxied peer has sent FIN.
  • Whether resumed/background threads retain both MCP runtimes and outbound HTTP clients longer than intended.

Related issues

  • #30408 — per-thread MCP processes never cleaned up
  • #33946 — multiple tasks duplicate MCP processes on Windows
  • #34658 — completed subagents leave STDIO MCP processes running
  • #38526 — current Windows Desktop local STDIO MCP process accumulation
  • #31376 — separate CLOSE_WAIT / pooled connection hang evidence in codex exec

The MCP process accumulation overlaps with those reports. The additional observation here is that, on current Windows Desktop, the app-server simultaneously accumulates local-proxy CLOSE_WAIT sockets, and those sockets are not owned by the MCP child processes.

View original on GitHub ↗

3 Comments

github-actions[bot] contributor · 11 days ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #38526
  • #38614
  • #38925
  • #38754
  • #38825

Powered by Codex Action

jdcodes1 · 10 days ago

Adding code-level corroboration from main @ 1f41cc5d92, plus a concrete mechanism that connects your two symptoms — the process trees and the app-server-owned proxy sockets are very plausibly the same leak seen from two sides.

The process half matches the in-session replacement leak. I traced this in detail on #38925 (macOS, same shape): the in-session MCP refresh path (McpRuntime::replacepublish) never shuts down superseded connections — McpConnectionSet::shutdown, which actually terminates stdio children, is called only from the session-end path — and the Drop fallback only cancels the startup token, which is a no-op once a server is running. Child termination then requires the last Arc<RmcpClient> clone to drop, and clones survive in cached bindings and the shared startup future. Your "32 sets" numbers add the second multiplier: connection sets are per thread session, so a Desktop app-server hosting many threads accumulates per-thread sets and per-refresh replacements within each thread. Full mechanism with line references: https://github.com/openai/codex/issues/38925#issuecomment-5312030670

The socket half has a direct code path to the same leak. Every MCP runtime refresh constructs a fresh McpRuntimeContext, and its constructor builds a brand-new HTTP client with its own connection pool:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/codex-mcp/src/runtime.rs#L497-L508

That context (and therefore the pool) is captured by the connection set built for that refresh. With HTTPS routed through your local proxy, each pool holds keep-alive sockets to 127.0.0.1:10888. When a superseded set leaks (per the above), its HTTP client is never dropped, so its pooled sockets are never closed — and once the proxy times out and closes its side, the app-server half sits in CLOSE_WAIT forever, owned by the app-server process exactly as you observed. Live sets' pools account for the ESTABLISHED population. So the correlation you were careful not to overclaim does have a concrete candidate mechanism: one retained Arc chain keeps both the child processes and the socket pools alive.

A discriminating check that doesn't require code changes: correlate socket-count growth with MCP refresh events rather than with tool traffic — e.g. watch netstat -ano | findstr 10888 while toggling something that forces an MCP runtime refresh (auth change, config touch, switching threads). If CLOSE_WAIT steps up with refreshes/thread-opens rather than with actual MCP usage, that's the pool-per-refresh signature. And the ultimate confirmation is shared with #38925: a fix that explicitly shuts down superseded connection sets on publish should flatten both curves at once — worth treating these as one bug for that reason, even if this issue stays open for the Windows/proxy evidence.

One independent improvement regardless: McpRuntimeContext::new could reuse a shared HTTP client (the factory it clones from is already long-lived) instead of building a pool per refresh — that bounds proxy socket usage even for the live set population, which your 36 ESTABLISHED suggests is already non-trivial.

tvivt · 10 days ago

I tested a user-level mitigation that does not require rebuilding Codex Desktop. I am using CodeGraph as the example.

Idea

Do not let Codex Desktop launch CodeGraph directly as a STDIO MCP server for every retained runtime/session. Instead, run one external CodeGraph STDIO process behind a local Streamable HTTP gateway, then configure Codex to connect to that HTTP endpoint.

Setup

Install mcp-proxy (for example with uv):

uv tool install mcp-proxy

Start one loopback-only gateway:

mcp-proxy --host 127.0.0.1 --port 18104 --pass-environment -- codegraph serve --mcp

Then replace the direct STDIO CodeGraph entry in ~/.codex/config.toml with:

[mcp_servers.codegraph]
url = "http://127.0.0.1:18104/mcp"
startup_timeout_sec = 120
tool_timeout_sec = 300
enabled_tools = ["codegraph_explore"]

On Windows I run the gateway from Task Scheduler at logon with:

  • MultipleInstances = IgnoreNew
  • a startup mutex / port-health check
  • pythonw.exe plus CREATE_NO_WINDOW, so no console window is shown
  • logs redirected to files under %LOCALAPPDATA%

The important property is that the scheduled task/proxy owns exactly one codegraph serve --mcp child. Codex can create multiple logical HTTP MCP sessions, but they share that backend.

Local validation

I opened two independent MCP sessions, ran tools/list and codegraph_explore through both, and then checked the process tree:

Passed: True
LogicalSessions: 2
CodeGraphBackendCount: 1
Endpoint: http://127.0.0.1:18104/mcp

This has prevented the previous multiplication of CodeGraph STDIO process trees and bounds the CodeGraph backend memory/process cost to one instance.

Limitation

This is containment, not a root-cause fix. It does not make Codex shut down superseded McpConnectionSet instances or their HTTP client pools. Therefore Codex-side sockets/client pools may still accumulate against the local gateway even though CodeGraph backend processes no longer multiply.

The upstream lifecycle fixes suggested above are still needed. This workaround is useful meanwhile for expensive STDIO MCP servers that support multiple logical sessions through mcp-proxy.