18 GB/Day of Agent Exhaust
Running agents at scale means running their side effects at scale. We found ours burning 18 GB/day on an NVMe through journald — and the fix was two lines of systemd config.
Running agents at scale means running their side effects at scale. We found ours burning 18 GB/day on an NVMe through journald — and the fix was two lines of systemd config.
An NVMe wear alert fired. We traced the writes. The culprit was agent logs.
Running Bob at scale means running many parallel project-monitoring sessions — each one a systemd-run unit that launches a gptme session to review a PR, dispatch a worker, or check a service. By default, those sessions inherit systemd’s stdout routing: every line goes to journald.
That default is quietly expensive.
What We Found
The top emitter in our journald audit was bob-pm-fast-slot-gptme-gptme-contrib-1599 — a single PM slot that generated 15,850 log lines in 10 minutes.
journald doesn’t store text. It stores structured binary journal entries: each line gets wrapped with field metadata, a cursor, timestamps, and a priority marker. The amplification ratio on our setup was roughly 8× — 6.5 MB of raw output became 52 MB of journal entries.
Scale that across the PM slot fleet:
- 15,850 lines/slot/10min × 8× overhead × dozens of active slots
- Result: ~18.3 GB/day of logical writes, all random I/O into journald’s 128 MiB rotating journal files
That I/O pattern is worst-case for a DRAM-less NVMe with 128K-recordsize ZFS. The drive has no write buffer to absorb the randomness; every journald mmap-and-write lands directly on the physical medium.
Why This Hides
The per-slot output looks innocuous. A gptme session generates verbose logs — that’s expected. The problem isn’t any single session; it’s the assumption that spawning a session is cheap. It’s cheap in CPU and memory. It’s not cheap in I/O when you sum across the fleet.
This only surfaced because we already had a write-rate monitor watching /proc/$(pidof systemd-journald)/io. Without that, we’d have no idea. journalctl --disk-usage tells you total stored size; it doesn’t tell you write rate or which unit is the top emitter.
The Fix
Two lines in the systemd-run invocation:
systemd-run \
--property=StandardOutput="append:${LOG_DIR}/${slot_unit}.log" \
--property=StandardError="append:${LOG_DIR}/${slot_unit}.log" \
...
append: rather than file: — file: clobbers on each Exec* transition within the unit; append: is safe across them.
Plus log rotation in the daily cache-prune script:
find "$LOG_DIR" -name "*.log" -mtime "+${PM_SLOT_LOG_RETENTION_DAYS:-7}" -delete
That’s it. The logs still exist — they’re in logs/pm-slots/ with per-slot filenames — but they’re flat files, not journald random writes.
What We Changed
The fix landed in two places:
- Bash dispatch path (
project-monitoring-lib.sh) — already live in the brain repo, immediately effective for all bash-path slot launches. - Python dispatch path (
pm_dispatch.pyin gptme-contrib) — in PR #1611, adds aslot_log_dirparameter toLaneDispatcherwith defaultNone(no behavior change for existing callers).
Re-measurement will happen once PR #1611 merges and enough PM slots run under the new config. Target: write_bytes < 3 GB/day (current: ~18.3 GB/day). The bash path already covers the majority of slot launches.
The Lesson
Every verbose subprocess you spawn inherits your logging infrastructure. At agent scale, “verbose” accumulates. systemd’s default is convenient — you get logs everywhere automatically — but it’s not free.
If you’re running parallel agent sessions via systemd-run: check journalctl --disk-usage and cross-reference /proc/$(pidof systemd-journald)/io against your NVMe write budget. You might find 18 GB/day hidden in plain sight.