Low-memory guard kills a backgrounded long build under healthy memory; running it detached (tmux) instead of run_in_background fixed it
Summary
While using Claude Code (Desktop, Code tab, Remote Control session) to drive a long-running background build inside a nested QEMU guest on a remote Linux host, a background Bash command supervised via run_in_background: true was repeatedly stopped by the low-memory guard ("Background command ... was stopped because the system is running low on memory") even though host-level memory (free -h → available) and guest-level memory both looked healthy at the time. Switching the exact same workload to run detached under tmux (started as a normal, non-backgrounded Bash call that returns immediately, then polled for a completion flag file every few minutes via ordinary foreground Bash calls) eliminated the kills entirely on the next attempt, at an equal-or-higher resource load.
This is not a definitive root-cause report — I could not inspect the guard's implementation or its exact trigger condition. It is a reproducibility report with a correlation that seems too strong to ignore, submitted so the team can decide whether it's worth investigating.
Environment
- Claude Code version: 2.1.267 (reported by the remote session; my own session's exact build wasn't captured)
- Host: Ubuntu 24.04.5 LTS (fresh install), Intel Core i5-4590 @ 3.30GHz (4 cores), 15 GiB RAM
- Workload: a nested QEMU/KVM guest (
qemu-system-x86_64 -accel kvm -m 3072 ...) building a Linux kernel withmake -j3(or-j2on one attempt) inside the guest; the guest was itself started and driven from a Python script invoked via the Bash tool - Session type: an interactive Claude Code session connected over Remote Control from a separate coordinating session; the affected commands were background Bash tool calls (
run_in_background: true) on the Remote-Control side
What happened (4 consecutive kills, then 1 success)
| # | Guest RAM | Guest make -j | Stage reached before kill | Guest-side memory at kill time | Host available at kill time |
|---|---|---|---|---|---|
| 1 | 4096 MB | 4 | shortly after dpkg-source -b began | not captured | not captured |
| 2 | 4096 MB | 3 | shortly after dpkg-source -b began | not captured | not captured |
| 3 | 3072 MB | 3 | mid-way through actual compilation (many .o files already built) | healthy (free: several GB free) | ~12 GiB available |
| 4 | 3072 MB | 2 (lighter than #3) | earlier than #3 — dpkg-source -b, before any real compilation | healthy (free 2.6Gi inside a 2.8Gi guest) | ~12 GiB available |
Attempt 4 is the detail that stood out: a lighter configuration (-j2 instead of -j3) died at an earlier, less resource-intensive stage than the heavier attempt (#3), while both guest and host memory looked fine by ordinary tools (free -h, ps). That inverse relationship (lighter workload → earlier failure) is what made me suspect the guard's decision wasn't actually tracking the real resource usage of the supervised process tree, but something about how the harness itself was watching the backgrounded command.
The fix that (so far, once) worked
For attempt 5, instead of launching the build as a run_in_background: true Bash call that the harness would supervise directly, I had it:
- Start the entire build (guest boot →
make -j3 deb-pkg→ post-install steps) inside atmuxsession via a single foreground (non-backgrounded) Bash call that itself returns immediately (tmux new-session -d -s buildsession '... ; echo DONE=$? > /tmp/status.txt'). - Never issue a long-lived
run_in_background: truewait on that build. Instead, check on it every few minutes via ordinary foreground Bash calls (tmux ls,caton the status file,free -h).
That run - same guest RAM (3072 MB), same -j3, the full ~109-minute build, at least as much real memory pressure as attempt #3 - completed with zero kills. Host available stayed around 9-10 GiB throughout, gradually and unremarkably, matching ordinary page-cache growth during heavy compilation I/O.
What I'm NOT claiming
- I don't have access to the guard's source or its exact metric/threshold, so I can't say why the
run_in_backgroundpath killed the process while the same load undertmuxpolling didn't. - I have not gone back and reproduced the kill under
run_in_backgrounda 5th time to rule out it being coincidence (e.g. a transient host-level event around that time that happened not to recur). n is small (4 kills, 1 success). - I don't know whether this is specific to a nested-QEMU workload, to Remote Control sessions, to this host, or general.
What would help
- Any documentation of what the low-memory guard actually measures for a backgrounded command (host-wide
MemAvailable? the process tree's RSS? something else?), and whether it behaves differently for commands started viarun_in_backgroundvs. a plain shell background job (tmux/nohup ... &) that the harness isn't directly supervising. - If this is a known false-positive pattern, a documented workaround (e.g. "detach long builds from run_in_background") would have saved a lot of the ~4 hours spent tuning
-m/-jbefore finding the actual fix.
Happy to provide more detail (exact commands, timestamps) if useful - this report is from a relayed cross-session investigation, so I don't have raw guard log output, only the timestamped narrative above.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗