# Subagents Are Zombie Processes

> Source: <https://victorvelazquez.dev/blog/your-subagents-are-zombie-processes/>
> Published: 2026-07-07 12:06:13+00:00

# Your subagents are zombie processes

I found it because the cursor was blinking.

The review was done — posted, merged, forgotten. But the session was still flickering, so I opened the background-tasks panel, and there they were: two agents, still running, five and a half hours after they had anything to do. Around a hundred thousand tokens, spent re-deriving an answer I’d already shipped. A week earlier, on another project, a single agent had spawned **106 subagents** for one web search. I find these the same way every time: by accident.

If you’re building anything multi-agent, you have these too. You just haven’t caught them yet.

## Nobody owned the children

Here’s the mechanics, because they’re mundane and that’s the point.

I asked an agent to review a big pull request by fanning out — a few subagents, each on a different dimension. Good pattern; it caught a real bug. One of those subagents parallelized *its* slice and spawned two of its own. Then the parent finished, reported up, and returned — without stopping the children it had started. It never treated their lifecycle as its job.

So they orphaned. Running, billing, invisible, one level below anyone who was watching. This wasn’t a bug in any single agent’s reasoning. Every agent in that tree did its job well. It was a gap in *ownership* — nobody was responsible for turning the children off.

## There was no switch

My first instinct was to find the setting. There isn’t one, and the search is worth sharing, because the absence is the story:

**Lifecycle hooks exist, but they’re the wrong shape.** They fire when an agent*finishes*and hand you its final message. They can’t enumerate a sibling that’s still alive, and can’t reach over to kill it. They react; they don’t reap.**There’s no max-runtime or auto-kill for subagents.** Nothing times out a hung child.**The platform has open bugs in exactly this spot**— finished subagents that never get reconciled and linger in a running state. So part of what I hit wasn’t my orchestration at all. The infrastructure has no reaper either.

That’s the uncomfortable takeaway for the whole field: we’re wiring up systems that spawn processes, and we haven’t built the process table.

## We solved this in 1985

An orphaned subagent is a **zombie process**. The vocabulary maps one-to-one:

- A child outlives its parent’s attention →
**orphan**. - Nobody collects its exit status →
**zombie**. - No supervisor adopts and cleans it up → no
.`init`

/reaper

Unix answered this decades ago with two ideas: a parent is expected to `wait()`

on its children, and if it doesn’t, `init`

adopts the orphans and reaps them. Multi-agent frameworks today reliably have neither. We skipped the lesson because “agent” sounded novel enough to feel exempt from operating-systems history. It isn’t. It’s `fork()`

with a language model attached and a credit card.

And the agent case is *worse* than the Unix one in three ways that matter:

**The leak is silent.** A zombie process shows up in`ps`

. An orphaned agent shows up as a bigger invoice next month.**The resource is money.** Idle isn’t free — a hung agent can keep thinking, and thinking bills.**There’s no default supervisor.** Your terminal reaps your child processes when it dies. Nothing reaps your agents.

## The fix is the old fix: ownership, delegated recursively

You don’t need the platform to grow a reaper. You need the thing Unix already assumes: **every parent owns its children, because the parent is the one holding their handles.**

That last clause is the whole insight. When I couldn’t kill the orphans, it was only because they were my *grand*children, one level below me. But their direct parent *did* hold their IDs. It could have stopped them. It just was never told that was its job.

So make it the job, and make the instruction travel. Every spawned agent carries a clause like this, with orders to pass it down to anything *it* spawns:

Lifecycle contract (include verbatim in any subagent you spawn):Do your task. Before you return, make sure any subagents or jobsyoustarted have finished or been stopped — you hold their handles, so stop any still running. Don’t return while leaving work alive; if a child is stuck, stop it rather than waiting on it. If you spawn helpers, pass them this same paragraph so the rule keeps propagating.

Because it propagates, it self-assembles into a teardown tree. Level four cleans five, three cleans four, the root cleans the top. No depth cap, no width cap — fan out as hard as the task deserves. The only invariant is that **nothing returns while its own subtree is still alive.**

Here’s the failure and the fix side by side:

```
WITHOUT the contract — the ghost          WITH the contract — clean teardown
────────────────────────────────         ──────────────────────────────────
you   ✓ done, moved on                    you   ✓ + sweep: verify all clear
└─ A  ✓ returned findings                 └─ A  ✓ stops B before returning
   └─ B  ✓ returned findings                 └─ B  ✓ stops C, D before returning
      ├─ C  ⟳ running 5h  ← orphan             ├─ C  ✓ stopped by B
      └─ D  ⟳ running 5h  ← orphan             └─ D  ✓ stopped by B

  each parent returned and forgot its       each parent reaps its own children,
  children → ghosts, billing, one           because it holds their handles →
  level below anyone watching               teardown climbs the tree
```

Think of it as nesting dolls: a child is only ever closed by the doll it sits inside. Belt and suspenders:

**The contract is the belt**— recursive self-teardown at every level.** A top-level sweep is the suspenders**— when the whole job is done, verify nothing’s still running, and if something is, surface it*now*, loudly, not on next month’s bill.

Be honest about what this is: a pattern, not a guarantee. It’s an *instruction*, so it depends on each agent honoring it, and a genuinely wedged child — or that platform reconciliation bug — can still slip through. That’s exactly why the rule is “stop, don’t wait,” and why the human-visible sweep stays. Don’t ship the belt and skip the suspenders.

## The real lesson

The bug that cost me a hundred thousand tokens wasn’t a reasoning error. It was an ownership gap — the kind of unglamorous, structural concern that “just make the model smarter” will never fix.

That’s the shift I keep relearning as I build with agents: a lot of the hard, valuable work isn’t prompting. It’s the systems discipline *around* the prompting — resource ownership, lifecycles, observability, accountability. The model is the easy part now. The engineering is making sure that when a fast, tireless, unaccountable thing spawns a hundred and six copies of itself, someone, at some level, is holding the handle that turns them off.

We should probably `wait()`

on our agents.
