Subagents are context isolation, not parallelism
Every dispatched worker pays a fixed setup cost before it does anything. Where the crossover sits, and why a ten-item batch ran faster serially.
The reason to dispatch a subagent is not that it runs alongside the others.
It is that everything it reads stays inside it, and only a result comes back.
The case this post is about
One 84-item conversion, shaped two ways:
const opts = { preambleTokens: 6_200, perItemTokens: 4_100, perItemSeconds: 55,
concurrencyCap: 12, parentTokensIfInline: 4_100 };
console.log("fan-out :", cost(84, { workers: 84, itemsPerWorker: 1 }, opts));
console.log("batched :", cost(84, { workers: 12, itemsPerWorker: 7 }, opts));
Eighty-four workers and twelve workers finish in the same wall clock, because the runtime caps concurrency at twelve either way — so the width written in the plan is not the width that runs.
The fan-out costs 446,400 more tokens to achieve that identical time, because the preamble is paid once per worker rather than once per wave.
This post follows those two shapes, because they buy exactly the same thing — 344,400 tokens of reading that never enters the parent context — and only one of them pays for it twice.
The symptom
"Fan this out" is the reflex when work looks repetitive, and the expected payoff is wall clock. Sometimes that arrives. Often what arrives is a run that costs more, takes about as long, and is harder to debug — and the reason is a cost that does not appear in the plan.
Every dispatched worker pays a fixed setup before it does any work. It needs the instructions for its task, the conventions it must follow, the shared context that makes its output fit with everybody else's. That preamble is identical across workers and is paid once per worker.
So the arithmetic is not total work ÷ N. It is:
N × (preamble + work)
which is worse than serial on cost, always, and only better on wall clock when work is large
enough to dominate preamble.
Why the obvious fix failed
The obvious fix is to make the preamble smaller, and it is worth doing — I trimmed one from 305,942 bytes to 280,963 by scoping it to the project type instead of shipping every variant to everyone.
That is an 8.2% reduction, against a rough estimate of 40% before measuring. Real, worth keeping, and nowhere near enough to change the arithmetic. A preamble that large still dominates any worker whose actual job is a few thousand tokens.
The second idea is to fan out more widely, on the theory that if parallelism is good, more is better. Width multiplies the preamble. It is the one lever that makes the dominant term worse.
The third is to batch items per worker — give each one five things to do instead of one, amortising the setup. This genuinely works and quietly gives up the thing you wanted: a worker holding five items is no longer isolated per item, and a failure in the third contaminates the remaining two.
The actual mechanism
Dispatch when the worker's reading is large and its answer is small.

That is the shape the mechanism is actually good at. A worker that reads eight hundred lines of a file to answer one question has consumed eight hundred lines of its own context and returned one sentence to yours. Run that inline and your context carries the eight hundred lines for the rest of the session, competing with everything that comes later.
The ratio is the criterion, not the repetition:
- read a lot, return a little → dispatch. This is the whole use case.
- read a little, do a lot → inline. The preamble is pure overhead.
- read a lot, return a lot → dispatch buys you nothing; the reading arrives anyway.
Note that none of those mentions speed. Speed is what you get when the work is also large and genuinely independent — a bonus on top of the isolation, not the reason for it.
The fix
The practical rule I settled on: dispatch for the reading, batch for the throughput, and never expect both from the same decision.
Two things that sharpened it.
Check what the work contends on before checking whether it is independent. Ten items with no logical dependency can still be perfectly serial in practice, because they share a quota, an account, or a resource with no clean retry point. Logical independence is necessary and not sufficient.
Concurrency is capped anyway. The runtime here caps simultaneous workers at roughly the number of available cores minus a couple. Passing a hundred items does not run a hundred workers; it runs about a dozen and queues the rest — so the width you wrote in the plan is not the width you get, and the preamble is still paid a hundred times.
The two costs pull in opposite directions, and once they are both in the same function the decision stops being a preference:
type Shape = { workers: number; itemsPerWorker: number };
function cost(items: number, shape: Shape, opts: {
preambleTokens: number; // paid ONCE PER WORKER, whatever it does
perItemTokens: number;
perItemSeconds: number;
concurrencyCap: number; // the runtime's cap, not the width you asked for
parentTokensIfInline: number; // what the reading would cost in YOUR context
}) {
const workers = Math.ceil(items / shape.itemsPerWorker);
const effective = Math.min(workers, opts.concurrencyCap);
const waves = Math.ceil(workers / effective);
return {
workers,
effectiveParallelism: effective,
tokens: workers * opts.preambleTokens + items * opts.perItemTokens,
seconds: waves * shape.itemsPerWorker * opts.perItemSeconds,
// The thing dispatch actually buys: none of the reading lands in the parent.
parentTokens: 0,
inlineParentTokens: items * opts.parentTokensIfInline,
};
}
The 84-post conversion, as one-worker-per-post and as a dozen batched workers:
const opts = { preambleTokens: 6_200, perItemTokens: 4_100, perItemSeconds: 55,
concurrencyCap: 12, parentTokensIfInline: 4_100 };
console.log("fan-out :", cost(84, { workers: 84, itemsPerWorker: 1 }, opts));
console.log("batched :", cost(84, { workers: 12, itemsPerWorker: 7 }, opts));
fan-out : { workers: 84, effectiveParallelism: 12, tokens: 865200, seconds: 385,
parentTokens: 0, inlineParentTokens: 344400 }
batched : { workers: 12, effectiveParallelism: 12, tokens: 418800, seconds: 385,
parentTokens: 0, inlineParentTokens: 344400 }
Identical wall clock, and the fan-out costs 446,400 more tokens — because 84 preambles were paid to get 12 workers. Both shapes buy the same thing, which is that 344,400 tokens of reading never enters the parent context. That is the decision: dispatch because of the last column, and then pick the shape from the second, because the width in the plan and the width at runtime are not the same number.
How we proved it
The measurement I trust is the one where I chose the slower-looking option and it was correct.
This article is one of ten written in a single session. Each needed prose, five generated images, and a verification pass — textbook fan-out material: ten independent items, no cross-item dependency.
I ran them serially, in one context.

The deciding factor was not the preamble arithmetic, it was contention. Image generation runs against a quota-bound account pinned to the project, and the convention is explicit that it runs serially rather than batched. Ten workers would have queued on one resource, and the failure mode is a partial batch with no clean point to resume from.
What I did dispatch was the reading — the question "does this rendered image show what the brief asked for". That is stateless, contends on nothing, and returns a verdict rather than an image. Read a lot, return a little.

The honest limitation, and it undercuts the whole apparatus: isolation does not eliminate the context problem, it relocates it. The scheduler still accumulates every result it collects, every decision about how the work was split, and the synthesis at the end. Ten workers each returning a tidy paragraph is still ten paragraphs in one context. A long enough run exhausts the one context that cannot be delegated, and the fan-out bought time rather than a solution.
The ceiling on "read a lot"
There is an upper bound on the whole technique that took me a while to notice: a worker's context is finite too.
"Read a lot, return a little" works while the reading fits in one worker. Past that, dispatch stops being isolation and becomes a smaller version of the original problem — the worker fills up, starts losing the early part of what it read, and returns a confident answer based on a partial view. The result is worse than doing it inline, because inline you would at least have noticed the volume.
And the worker cannot solve this itself, because it cannot delegate: there is no second level to push the excess down to. Whatever it cannot hold, it simply does without.
So the reading has to be decomposed before it is dispatched, not after. Ten files, one worker each, with the scheduler combining ten short answers — rather than one worker asked to read all ten and summarise. Both look like "read a lot, return a little" in the plan. Only the first one is actually bounded.
What I still get wrong
I reach for dispatch when work looks repetitive, and repetition is not the criterion — it is just the most visible property. Ten identical small edits are repetitive and belong inline; one enormous investigation is not repetitive at all and is the best dispatch candidate I have.
The tell I have learned to watch for is asking "should I fan this out?" before asking "how much will each worker read, and how much will it say back?" When the second question comes first, the answer to the first is usually obvious and frequently no.
What this generalizes to
Reach for a subagent when you want its reading kept out of your context, not when you want the work to go faster. Judge it on the ratio of what the worker consumes to what it returns, and remember that every worker re-pays the setup — so width makes the dominant cost worse, not better.
Then look at what the work contends on before you look at whether it is independent. A serial run you can resume beats a parallel one you cannot, and the plan that says ten workers will usually get you about a dozen at a time regardless of what you wrote.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".