Codex Desktop on Windows accumulates Node REPL/MCP/stdio app-server helper stacks during long multi-thread sessions
Summary
Codex Desktop on Windows becomes progressively sluggish during long-running Desktop usage with many project threads/subagents/tool-enabled turns. Restarting Codex clears the condition, so this does not look like an unavoidable model or OS limitation.
Live diagnostics from the affected host show that the strongest local signal is not an enormous state_5.sqlite thread table. Instead, Codex has accumulated many helper process stacks:
- 22
node_repl.exeprocesses - 21
node.exe ./mcp/server.cjs --stdioprocesses - 22 nested
codex.exe app-server --listen stdio://processes - large Desktop renderer/app-server memory
- large local rollout/log history
This looks like a Windows instance of the broader helper/MCP/app-server lifecycle problem: helper stacks are scoped too narrowly to sessions/tools, or are not reliably torn down after threads/subagents/tools finish.
Environment
- App: Codex Desktop on Windows
- OS: Microsoft Windows 11 Pro, version 10.0.26200, 64-bit
- Codex package:
OpenAI.Codex_26.601.2237.0_x64__2p2nqsd0c76g0 - Bundled app-server CLI:
codex-cli 0.136.0-alpha.2 - Affected usage pattern: long-lived Desktop session, many local project threads, subagent/tool-heavy work, Browser/Node REPL/MCP-capable sessions
Observed behavior
After several hours of normal Codex Desktop usage, the app becomes heavy/sluggish even though restarting Codex restores responsiveness.
At capture time, Windows still had physical memory available:
Total physical memory: 31.14 GB
Free physical memory: 5.19 GB
Used physical memory: 83.3%
Total virtual memory: 62.29 GB
Free virtual memory: 6.44 GB
The local thread DB was modest:
state_5.sqlite size: 3.8 MB
threads_total: 346
active/unarchived threads: 292
archived threads: 54
active metadata chars:
title: 119,865
preview: 404,887
first_user_message: 422,971
The thread/list query plan still has the known archived index plus temp sort shape:
SEARCH threads USING INDEX idx_threads_archived (archived=?)
USE TEMP B-TREE FOR ORDER BY
However, with only 346 thread rows and a 3.8 MB DB, this does not look like thread-list scale alone.
The stronger signal is the process/log/session footprint:
logs_2.sqlite: 676.87 MB, 307,649 log rows
sessions rollout files: 292 files, 3540.7 MB total, largest 235.68 MB
Live Codex-related process aggregate:
desktop_gpu count=1 private=301.8 MB working=218.8 MB
desktop_renderer count=2 private=1714.5 MB working=1513.3 MB
desktop_utility count=2 private=35.3 MB working=34.9 MB
main_app_server count=2 private=699.2 MB working=560.0 MB
mcp_server_cjs count=21 private=435.4 MB working=596.5 MB
node_repl count=22 private=53.6 MB working=137.2 MB
npm_codex_cli count=2 private=57.5 MB working=116.3 MB
other_codex_related count=4 private=413.4 MB working=241.1 MB
stdio_app_server count=22 private=312.0 MB working=518.3 MB
The repeated helper pattern was:
node_repl.exe
node.exe ./mcp/server.cjs --stdio
codex.exe app-server --listen stdio://
There were 21-22 concurrent instances of each helper class. Several had been alive for hours under the long-lived Desktop/app-server process.
Recent logs_2.sqlite rows were also dominated by TRACE/INFO logs from the main app-server process. In the last 2000 rows:
log 980
codex_otel.log_only 302
codex_otel.trace_safe 297
codex_api::endpoint::responses_websocket 292
codex_core::stream_events_utils 25
codex_core_skills::loader 24
codex_api::sse::responses 21
codex_mcp::connection_manager 18
codex_app_server::outgoing_message 16
Expected behavior
Codex Desktop should keep helper process count bounded during long-running multi-thread sessions.
In particular:
- completed/closed/superseded threads should not leave their own
node_repl/MCP/stdio app-server stacks alive indefinitely - subagents should not multiply helper stacks unless they are needed, and those stacks should be cleaned up after the subagent exits
- project/workspace-level helpers should be pooled or reused where possible
- large
logs_2.sqliteand rollout histories should not be on the active UI/send path except through bounded, lazy reads - restarting Codex should not be the only way to return to a clean helper/process baseline
Actual behavior
A long-lived Desktop session accumulated 21-22 concurrent instances of each helper class (node_repl, MCP server.cjs, nested stdio app-server), while Desktop renderer/app-server memory and local logs/session history were also large.
The active state_5.sqlite thread table was not large enough to explain the slowdown by itself.
Why this looks like a product lifecycle bug
The usage pattern is heavy, but normal for Codex:
- many project threads
- long local sessions
- subagents
- browser/Node REPL/MCP-capable tasks
- repeated tool-heavy audit/review work
Those should not cause helper process count to grow roughly with historical thread/tool usage. The local process tree should converge back to a bounded baseline after tasks complete.
The fact that a full Codex restart restores responsiveness suggests retained local process/renderer/session/log state rather than a model-side latency issue.
Related issues checked
This appears related to, but not fully covered by:
- #20883: project-scoped MCP process pool instead of per-session MCP
- #25015: Linux app-server leaks MCP process stacks for subagents
- #25744: macOS accumulates Computer Use / MCP helper processes
- #26015: Windows retains/increases memory after opening long tool-heavy thread
- #24427: Windows many project threads slow active sends/responses
- #24510: active thread metadata/thread-list scalability
- #20213:
logs_2.sqliteTRACE logging / SQLite contention
This report adds a current Windows Desktop 26.601 live-process snapshot showing node_repl/MCP/stdio app-server helper stack accumulation even though state_5.sqlite itself is small.
Diagnostic commands used
These are safe to run locally, but users should redact paths/usernames before posting output.
Count process classes:
$procInfo = Get-CimInstance Win32_Process |
Where-Object {
$_.CommandLine -match 'Codex|codex|node_repl|mcp/server.cjs|@openai/codex' -or
$_.Name -match 'Codex|codex|node_repl'
}
$perf = @{}
Get-Process | ForEach-Object { $perf[$_.Id] = $_ }
$rows = foreach ($w in $procInfo) {
$p = $perf[[int]$w.ProcessId]
$cmd = [string]$w.CommandLine
$cat =
if ($cmd -match 'app-server --analytics-default-enabled') { 'main_app_server' }
elseif ($cmd -match 'app-server --listen stdio://') { 'stdio_app_server' }
elseif ($cmd -match 'node_repl.exe') { 'node_repl' }
elseif ($cmd -match 'mcp/server.cjs --stdio') { 'mcp_server_cjs' }
elseif ($cmd -match 'WindowsApps\\OpenAI\.Codex_.*--type=renderer') { 'desktop_renderer' }
elseif ($cmd -match 'WindowsApps\\OpenAI\.Codex_.*--type=gpu-process') { 'desktop_gpu' }
elseif ($cmd -match 'WindowsApps\\OpenAI\.Codex_.*--type=utility') { 'desktop_utility' }
else { 'other_codex_related' }
[pscustomobject]@{
Category = $cat
Id = $w.ProcessId
PPid = $w.ParentProcessId
PrivateMB = if ($p) { [math]::Round($p.PrivateMemorySize64 / 1MB, 1) } else { 0 }
WorkingMB = if ($p) { [math]::Round($p.WorkingSet64 / 1MB, 1) } else { 0 }
Start = if ($p) { $p.StartTime } else { $null }
}
}
$rows |
Group-Object Category |
ForEach-Object {
[pscustomobject]@{
Category = $_.Name
Count = $_.Count
PrivateMB = [math]::Round(($_.Group | Measure-Object PrivateMB -Sum).Sum, 1)
WorkingMB = [math]::Round(($_.Group | Measure-Object WorkingMB -Sum).Sum, 1)
OldestStart = ($_.Group | Sort-Object Start | Select-Object -First 1).Start
}
} |
Sort-Object Category |
Format-Table -AutoSize
Inspect local thread DB size and counts:
import os, sqlite3
p = os.path.expandvars(r'%USERPROFILE%\.codex\state_5.sqlite')
con = sqlite3.connect(f'file:{p}?mode=ro', uri=True)
cur = con.cursor()
print('size_mb', round(os.path.getsize(p)/1024/1024, 2))
print('threads_total', cur.execute('select count(*) from threads').fetchone()[0])
print(cur.execute('select archived, count(*) from threads group by archived').fetchall())
print(cur.execute('explain query plan select id,title,preview,first_user_message,cwd,created_at_ms,updated_at_ms,model_provider from threads where archived=0 and preview <> "" and model_provider in ("openai") and created_at_ms < 9999999999999 order by created_at_ms desc limit 50').fetchall())
con.close()
Suggested fix area
- Bound helper ownership to a project/workspace-level pool instead of per loaded thread/session where possible.
- Ensure
node_repl, MCP stdio servers, and nestedcodex app-server --listen stdio://children are torn down when the owning session/subagent/tool is complete, aborted, closed, or superseded. - Add process-count/leak tests for repeated subagent/tool/browser sessions on Windows.
- Make Desktop expose a diagnostic surface showing which thread/session owns each helper stack.
- Consider bounding default TRACE logging into
logs_2.sqlite, or moving noisy stream logs off the active shared SQLite path.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗