I run a small fleet of subagents most days — a digest curator, a blog writer, a deploy runner, a handful of one-off research tasks that spin up, do their job, and disappear. The first time I actually totaled the token bill for a “simple” fan-out task — five agents summarizing five sources, then a sixth agent merging their output — I was not happy. Most of that spend wasn’t reasoning. It was five agents each re-reading context they didn’t need, and a merge step re-reading everything twice. That’s the token bleed, and it turns out it comes from exactly two sources, both of which are fixable without touching the model.
The two leaks
Multi-agent systems waste tokens in two specific, compounding ways:
- Unstructured parallel activation — every agent in the graph fires regardless of whether its inputs are actually ready, so idle agents burn tokens re-polling or re-reading stale context just to figure out they have nothing to do yet.
- Unrestricted context sharing — the default pattern is “give every agent the full accumulated conversation,” when most agents only need the two or three facts relevant to their specific subtask.
Neither of these is a model problem. They’re both orchestration problems, and orchestration is something a tech lead can actually control, unlike model pricing.
Phase-scheduled activation
A recent research pattern I’ve started borrowing from — phase-scheduled multi-agent systems (PSMAS) — treats agent activation as a scheduling problem instead of a “spawn everything and let it sort itself out” problem. Each agent gets a phase derived from the task dependency graph: an agent whose inputs won’t be ready until stage 3 doesn’t wake up and poll during stages 1 and 2, it gets a compressed context summary and stays idle. The published numbers on this are a 27.3% average token reduction with task performance staying within about 2 percentage points of a fully-activated baseline — which is a genuinely good trade, since that “fully-activated” baseline is the wasteful default most of us ship first.
In practice this looks like the difference between:
// naive: every agent starts immediately, all reading the same growing context
const results = await Promise.all(agents.map(a => a.run(fullContext)))
and:
// phase-scheduled: agents only wake when their dependencies resolve,
// and only receive the slice of context their phase actually needs
const results = await pipeline(
stages,
(input, stage) => agent(stage.prompt, { context: stage.contextSlice(input) })
)
The second version isn’t more code. It’s the same orchestration primitive most agent frameworks already give you — a pipeline instead of a Promise.all — used deliberately instead of by default.
Context pruning: where and when
The second leak — every agent inheriting the full context — needs a pruning policy, not just a smaller starting prompt. The pattern that’s held up well in practice: trigger pruning automatically once context hits somewhere between 80–95% of your budget, not at a fixed token count, because “budget” varies by model and by how much headroom the next step needs. Two distinct mechanisms do different jobs here and get conflated constantly:
- Truncation and summarization control what leaves the context window — collapsing old turns once they’re no longer actionable.
- Pruning controls what enters it in the first place — filtering a tool’s raw output down to the fields the next step actually consumes, before it ever gets appended.
The second one is where most of the actual savings live, because tool outputs are almost always the biggest single contributor to context bloat — a directory listing, a full API response, a page of logs — and 90% of that payload is never read by the agent that receives it. A lightweight relevance filter between “tool ran” and “tool output enters context” pays for itself immediately:
async function toolResult(raw, task) {
const relevant = await scoreRelevance(raw, task) // cheap, small-model or heuristic pass
return relevant.filter(f => f.score > THRESHOLD).map(f => f.field)
}
This is a smaller model or even a regex/heuristic pass doing triage before the expensive model ever sees the payload — the same instinct as a cache in front of a slow database, applied to context instead of data.
What this looks like on my own fleet
The workflow I run digest and blog tasks through already uses pipeline-based fan-out instead of barrier-based parallel() for exactly this reason: a barrier forces every stage to wait for every other stage’s slowest agent, which means every agent’s context sits open and growing the whole time everyone else finishes. Pipelining lets each item flow through its stages independently — agent A can be summarizing while agent B is still fetching, and neither one is holding open context it doesn’t need. Reserving true barriers for the one or two places that genuinely need cross-item context (deduplication before a final merge, mostly) instead of using them as the default is worth more than any single prompt-engineering trick I’ve tried.
The checklist
Before scaling up a multi-agent pipeline, three questions are worth asking, in order:
- Does every agent need to be active right now? If not, phase it — idle agents should get a summary, not a full replay.
- Does every agent need the full context, or a slice of it? Default to a slice; widen only when an agent actually fails from missing information.
- Is that barrier load-bearing? If a
parallel()call exists only because it was the first pattern you reached for, replace it with a pipeline and measure the difference.
None of this requires a different model or a bigger budget. It requires treating context the way you’d treat any other scarce, shared resource — with a scheduler and an eviction policy, not an all-you-can-eat default.