CLI: subagents memory leak
What version of Codex CLI is running?
0.130.0
What subscription do you have?
Pro Lite
Which model were you using?
_No response_
What platform is your computer?
Apple Silicon
What terminal emulator and version are you using (if applicable)?
_No response_
Codex doctor report
What issue are you seeing?
Spawning many subagents causes the native codex process memory and thread count to grow rapidly. The CLI can become temporarily unusable and may eventually exit/crash.
This appears to happen inside the native codex process, not in the Node wrapper or external child processes.
Observation:
codexRSS grew from ~23 MB to 315 MB+vmmapphysical footprint reached ~363.7 MBMALLOC_SMALLresident grew from ~29.8 MB to ~229.0 MB- malloc allocation count grew from ~79k to ~691k
- thread count grew from 22 to 68
When the session becomes large enough, RAM leak worsens indefinitely, eventually emitting "codex(...) MallocStackLogging: can't turn off malloc stack logging because it was not enabled." and make system unusable. (See #17139 )
What steps can reproduce the bug?
Spawn subagents in a large session
What is the expected behavior?
Subagent fanout should have bounded memory growth.
Completed subagents should not remain fully live in memory indefinitely. Their event queues, session loops, thread resources, and model/tool state should either be drained, cold-unloaded, or released after completion while preserving enough metadata/results for the parent thread.
The CLI should remain responsive and should not exit/crash.
Additional information
Cause 1: v1 child event queue may not be drained
codex::spawn creates a per-session unbounded event channel:
session/mod.rs:482, async_channel::unbounded()
session/mod.rs:747, next_event() consumes from that receiver.
When a child thread is spawned, the thread manager consumes the initial SessionConfigured event, but it does not drain later child events:
thread_manager.rs:1197
thread_manager.rs:1245
The completion watcher only subscribes to child status, waits for a terminal status, and injects a completion message into the parent. It does not consume the child event stream:
agent/control.rs:320
agent/control.rs:943
agent/control.rs:1012
There is also TODO about this:
agent/control.rs:308
Cause 2: completed child sessions are not released
The completion watcher notifies the parent, but the child session remains live. Actual release appears to happen through the explicit close path:
agent/control.rs:715, shutdown_live_agent
agent/control.rs:737, close_agent
agent/control.rs:79, spawn slot reservation
agent/control.rs:99, release path
This means terminal children can keep their session loop, event channel, metadata, and associated runtime allocations alive until the user or client explicitly closes them.
- Subagents getting the whole session transcript
6 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Confirming repeated native
codexOOM kills on Linux with Codex CLI 0.144.1 and 0.144.3.Retained kernel journals contain three events in which the kernel recorded
constraint=CONSTRAINT_NONE,global_oom, and killed the nativecodexprocess withSIGKILL:| Confirmed event | CLI/session state | Cumulative Codex agent/session tree | Rollout set on disk | Native
codexanonymous RSS ||---|---|---:|---:|---:|
| 1 | 0.144.1 | 34 nodes | 154.1 MiB | 3.42 GiB |
| 2 | 0.144.1 | 72 nodes | 345.6 MiB | 3.35 GiB |
| 3 | 0.144.3 process after resuming root state created on 0.144.1 | 155 nodes | 1,022.6 MiB | 2.84 GiB |
The final two events belong to the same root session across a restart/resume. The tree counts are cumulative root, child, and nested session nodes, not concurrent workers.
At the three events, the host had 11.6 GiB usable RAM, no swap, approximately 3.9–4.1 GiB of shared/tmpfs memory, and approximately 0.10–0.15 GiB free. The kernel OOM task tables showed
codexas the largest individual anonymous-memory process. Itsoom_score_adjwas 0.The shell printed only
Killed codex. Codex logs ended approximately 15 seconds, 4 minutes 35 seconds, and 14 seconds before the corresponding kernel records. I found no Rust panic, backtrace, fatal Rust error, clean-shutdown marker, or Codex coredump for these events.Environment: Linux x86_64, GNU Screen 5.0.1, multi-agent CLI workload.
Sanitized
codex doctor --jsonsummary:No raw doctor output, logs, rollout files, terminal snapshot, process listings, or log database are attached because they contain local paths, project content, and session metadata.
Another Linux data point, with kernel-side evidence for how bad this gets when MCP servers are configured.
Environment
codex-cli0.144.3 (npm, x86_64 musl vendor build)config.toml: 2 stdio (one of them loads a local embedding model), 1 streamable-httpWhat happened
A research fan-out with 12 parallel
codex execsubagents (read-only, web/GitHub research). Within ~10 minutes the box allocated ~30-40 GB and went into a global memory livelock. It was unreachable for 8 h 35 min, then recovered on its own.This matches the mechanism described in #23749: each subagent receives the full parent transcript and starts its own complete MCP stack. With K subagents that is K × the entire stack, including K copies of an embedding-model-backed server.
Timeline (from
sar, 10-minute sampling)| time | mem avail | page cache | load-1 | blocked | majflt/s | pgpgin/s |
|---|---|---|---|---|---|---|
| 12:00 | 45.3 GB | 31.0 GB | 0.94 | 0 | 0.00 | 0.7 |
| 12:10 | 39.8 GB | 19.7 GB | 1.16 | 0 | 0.23 | 1,684 |
| 12:20 | 2.06 GB | 4.0 GB | 11.6 | 64 | 213 | 59,797 |
| 12:40 | 1.94 GB | 3.9 GB | 674 | 87 | 4,044 | 1,196,746 |
| 21:10 | 29.3 GB | 7.3 GB | 0.67 | 0 | 24.7 | 11,144 |
sadccould not write a single sample between 12:40 and 21:00 despite a 10-minute timer.Independent confirmation of the freeze duration
An unrelated container logged a line whose application-internal timestamp is
12:25:42, while the Docker-side write timestamp is21:00:06. The process was blocked for 8.5 hours before it could flush one line to stdout.Why the OOM killer took 8.5 hours
The kernel kept reclaiming page cache instead of killing anything (cache is always nominally reclaimable), and with only 512 MB of swap there was nowhere to put anonymous memory. Cumulative PSI
fullmemory stall was ~8.95 h. It eventually fired:The moment the global OOM killer ran, the machine recovered. So this is not only "codex uses a lot of memory" - with no cgroup limit on the user session it degrades into a whole-host livelock rather than a failed codex run.
Peak of the cgroup the fan-out ran in: 64.5 GiB.
Workaround
Disabling all MCP servers for fan-out runs:
Research subagents do not need local MCP context anyway.
Note on a related footgun:
-c mcp_servers="{}"looks like it should clear the table but is a no-op on 0.144.3 -codex mcp list -c mcp_servers="{}"still reports every server asenabled. Only the per-serverenabled=falseform works. A profile file layered via--profilealso did not toggle them in my testing. Happy to open that separately if it is not already tracked.Suggestion: sharing one MCP stack across subagents of the same run, or at minimum a documented per-run switch, would remove the K× multiplier that makes this dangerous on machines that otherwise have plenty of RAM.
Hit this at 17.3 GB RSS after a 32 hour multi-agent CLI session. Root cause:
Every observed thread gets a lazily created
ThreadEventChannel(tui/src/app/thread_routing.rs) that buffers notifications while the thread isn't the active view. Channels for closed background agents are never removed. The buffer is also bounded by entry count (THREAD_EVENT_CHANNEL_CAPACITY = 32768, raised from 1024 in #9957), not bytes, and bufferedItemCompletednotifications carry full payloads (aggregated_outputis ~1MiB, MCP results can be bigger). So a finished subagent can leave gigabytes pinned until the session ends.vmmapon the 17.3 GB process shows 12.6 GB of MALLOC_LARGE across 1115 allocations (~11 MB average), which lines up with retained buffered payloads. Probably also behind #23749, #24668 and #20740.I've been running this fix locally: free the channel when a background thread emits
ThreadClosed(switching to it later just backfills from server history), plus a smaller capacity. https://github.com/Patchy/codex/commit/97dccc8fa5 if useful.This matches what I hit. In my run the retained memory was dominated by buffered
ItemCompletednotifications from finished backgroundcodex execsubagents — the tool-call results in those payloads are the largest contributor, well past the ~1 MiBaggregated_output, so an entry-count cap rather than a byte cap let a handful of closed agents pin most of the resident growth.Both parts of your fix look independently useful: freeing the channel on
ThreadClosedstops retention from scaling with the number of finished subagents, and the 32768 → 4096 cap bounds the live-thread case where a single busy agent buffers large results.Any chance you'd open it as a PR against
openai/codex? It currently only lives in your fork, and #23749 / #24668 / #20740 look like the same root cause.Thanks for confirming. I'd happily open a PR but contribution rules say PRs are invite only (docs/contributing.md), uninvited ones get closed. i see @etraut-openai triaged this issue before, so flagging the root cause analysis above. The change is contained to the TUI app layer (thread_routing.rs plus one constant). Also the const change to THREAD_EVENT_CHANNEL_CAPACITY is more of a arbitrary suggestion i'm just running with.