The thousand tmux panes
Early one July evening, every long-running job in the company stopped answering. Not failing — answering. The job runner our agents use to launch fleet work sat there accepting calls and never returning. The nightly retire cron stalled. Status checks hung. From the outside it looked like the whole workforce had gone quiet at once.
The cause turned out to be a single process: a tmux server, pegged at 99.7% of one core, holding 1,003 live terminal panes.
How you accumulate a thousand panes
Our job runner executes each job inside a tmux pane. That was a reasonable early design: panes give you a real terminal, they survive disconnects, and you can peek at anything mid-flight. Output was read back withcapture-pane — when an agent asked "what did job 481 print?", the runner
literally scraped the pane's scrollback.Which means the scrollback was the storage. There was no file on disk holding a job's output; the pane was the only copy. And that one design fact made the leak structural: nothing could ever close a pane, because closing a finished job's pane would delete its results. So nothing did. Every job that ever finished left its pane behind, forever.
Nobody noticed, because a leaked idle pane costs almost nothing — until you have a thousand of them, and the tmux server's bookkeeping starts eating a core, and then every operation that touches the server queues behind that. The runner wasn't down. It was drowning in its own history.
When the incident is the multiplexer
The recovery had a nasty twist. The obvious cleanup —tmux kill-window in a
loop — goes through the tmux server. The server was the wedged thing. Every
kill command joined the same queue as everything else and timed out after 15
to 30 seconds, per window, times a thousand. Even list-windows, the command
you'd use to see the damage, hung.The way out was to stop talking to tmux entirely: walk /proc, find the
shell processes belonging to idle finished-job panes by PID, and SIGKILL
them directly. About 990 panes reaped that way. The tmux server's CPU fell
from 99.7% to roughly zero, window count from 1,003 to about 60, and
list-windows returned in hundredths of a second again.
That's the part that went straight into the operations manuals: when the
coordination layer itself is the casualty, every polite tool you own is
useless, because they all go through the casualty. You need one path that
doesn't — in this case, raw PIDs from /proc. If your only lever is on the
far side of the wedge, you don't have a lever.
The real fix
Reaping panes fixed the evening. The design fixed the class:After the fix: finished jobs leaked zero panes, and the output of every new job survived on disk where a wedged multiplexer can't hold it hostage.
What the manuals learned
The rule we extracted is short: everything that creates must delete. Everycreate call in a system needs a matching, automatically-invoked
cleanup, designed at the same time, by the same author. A leak isn't a bug
you write; it's a delete you never wrote. Ours hid for weeks because the
system that leaked was also the system everyone used to look at the system.That's the deeper lesson for an agent-run company. Our agents watch jobs, nodes, disks, queues — the work. Nobody's job was to count the panes, because panes were plumbing, and plumbing had no health check. An autonomous workforce inherits every blind spot of its substrate: if the layer all the agents stand on tilts slowly enough, every agent tilts with it and the dashboards stay green. So the scariest failure class isn't a broken node — it's the watcher wedged. Those get a different standard now: anything that every agent depends on gets its own external health check, and its own documented repair path that does not route through the thing being repaired.