[Windows Desktop 26.810.7004] Thread-scoped STDIO MCP processes and local-proxy CLOSE_WAIT sockets accumulate
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:
- A new set of local STDIO MCP process trees is started for many threads/tasks and old sets remain alive.
- The Codex app-server accumulates both established and
CLOSE_WAITTCP 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:10888were owned bycodex.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
- On Windows, configure multiple global local STDIO MCP servers.
- Configure Codex traffic to use a local HTTP proxy.
- Fully start Codex Desktop.
- Open, resume, or switch among multiple local tasks over time.
- Let completed or idle tasks remain in the task list.
- Inspect direct and indirect descendants of the Desktop app-server.
- 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_timeoutmode 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 incodex 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.
3 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
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::replace→publish) never shuts down superseded connections —McpConnectionSet::shutdown, which actually terminates stdio children, is called only from the session-end path — and theDropfallback only cancels the startup token, which is a no-op once a server is running. Child termination then requires the lastArc<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-5312030670The 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 inCLOSE_WAITforever, owned by the app-server process exactly as you observed. Live sets' pools account for theESTABLISHEDpopulation. So the correlation you were careful not to overclaim does have a concrete candidate mechanism: one retainedArcchain 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 10888while toggling something that forces an MCP runtime refresh (auth change, config touch, switching threads). IfCLOSE_WAITsteps 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 onpublishshould 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::newcould 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 36ESTABLISHEDsuggests is already non-trivial.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):
Start one loopback-only gateway:
Then replace the direct STDIO CodeGraph entry in
~/.codex/config.tomlwith:On Windows I run the gateway from Task Scheduler at logon with:
MultipleInstances = IgnoreNewpythonw.exeplusCREATE_NO_WINDOW, so no console window is shown%LOCALAPPDATA%The important property is that the scheduled task/proxy owns exactly one
codegraph serve --mcpchild. Codex can create multiple logical HTTP MCP sessions, but they share that backend.Local validation
I opened two independent MCP sessions, ran
tools/listandcodegraph_explorethrough both, and then checked the process tree: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
McpConnectionSetinstances 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.