Back to blog
Series · Day 23
Multi-Agent Systems in 30 Days
View all lessons →
Fan-out and fan-in

Day 23: Fan-out is cheap, fan-in is where the bill arrives

Spawning eight agents now takes one line, whether that's Claude Code subagents, a workflow script or your own orchestrator. Parallelism got cheap. Merging didn't. If you don't design the merge step on purpose, your parallel run can be slower and more expensive than a single agent working alone, and it can also ship bugs that the single agent would have caught.

1. The 8-agent refactor

The job: move a mid-sized TypeScript service off scattered SQL helpers and onto a repository layer. It looked like a textbook parallel task, so I split it by feature across 8 subagents: auth, billing, notifications, search, admin, reporting, webhooks and onboarding. Every agent got a clear brief and the same coding conventions.

Generation took about four minutes, and all eight came back green on their own tests. It looked like a win until I started merging. Reconciling took over an hour, which was longer than an earlier attempt where one agent did the whole refactor serially. The numbers below are approximate and come from my own session logs. Read them as the shape of the problem, not as a benchmark.

The agents weren't the bottleneck. The bottleneck was putting their work back together.

2. The pattern: fan-out and fan-in

  • ▹Fan-out: an orchestrator splits the work across N agents that run in parallel.
  • ▹Fan-in: a single step, the reducer, collects those N results, reconciles them and decides what ships.
  • ▹Days 20–22 covered roles, handoffs and orchestrators. Fan-out/fan-in is what an orchestrator is doing any time it hands work to more than one worker at once. The handoff stops being one-to-one and becomes many-to-one.

Most agentic content only covers the fan-out half: the screenshot of 10 agents busy at once, or the demo that carves a task into tidy subtasks. Fan-in gets one line, 'then combine the results', and that line hides most of the engineering.

3. Where the time and tokens actually went

When I replayed the reconcile tail, almost every minute fell into one of four kinds of fan-in pain. The percentages are my rough estimate of each one's share of wall-clock time in that run.

  • ▹Edit collisions on shared files took the biggest share, roughly 40%. Five agents edited the same config. Three added overlapping types to a shared types file. Four touched the route table and the barrel exports (index.ts). Each edit was reasonable on its own, but together they conflicted.
  • ▹Duplicate findings in different words took roughly 15%. Three agents noticed the connection pool wasn't closed in tests. Each one described it differently and proposed a different fix, so someone had to work out that it was a single issue.
  • ▹Contradictory claims and opposite fixes took roughly 20%. One agent made a shared helper async. Another wrapped the same helper to keep it sync. Both changes were correct inside their own slice, and they couldn't both be true.
  • ▹Rebuilding context in the reducer took roughly 25%. Each agent handed back a diff and a short summary without the reasoning behind it. To judge a conflict, the reducer had to re-read the files and reconstruct why each agent made its choice. The agents had already done that work, so I paid for it twice.

The token count shows the same thing. The reducer, which I'd treated as overhead, used more tokens than any single worker. In my logs it came to about three workers' worth, because it kept loading overlapping file contents to settle conflicts. Fan-out spread the cost across the workers, and fan-in piled it into one context window.

4. The bug nobody caught

The second failure came the same week and was worse, because nothing looked wrong. I fanned out review agents over a large PR, one slice per directory. Every reviewer approved its slice, and every slice was locally correct.

The real defect spanned two files. In slice A, a repository method stopped throwing NotFoundError and started returning null. In slice B, a handler still wrapped the call in try/catch and mapped the error to a 404. After the change, a missing record came back as null and the endpoint returned a 200 with an empty body. Reviewer A saw a clean contract change. Reviewer B saw an untouched handler that looked fine. No reviewer had both files in context.

The takeaway is that your partition decides which bugs nobody can see. This is Conway's Law applied to agent topologies: organisations ship systems that mirror their communication structure, and agent swarms review code that mirrors their slicing. A defect that crosses a slice boundary falls between reviewers by design. More reviewers won't close the gap, because every new one is scoped to a slice too.

5. The rule: partition by blast radius, not by task

Draw the dependency graph before you spawn anything. Then cut where results will combine cleanly: disjoint file or module ownership, with narrow interfaces between the parts. Don't cut where the task is easiest to describe. Feature splits look tidy in a plan, but every feature runs through the same shared spine of config, types, routes and the DB layer, and that's where feature agents collide.

  • ▹Left: four agents, four write paths into the same files, so merge conflicts are guaranteed.
  • ▹Right: each agent owns a disjoint set of files, and every shared file has exactly one writer. Agents communicate through an interface agreed before fan-out, not by editing the same file.
  • ▹Rule of thumb: if two slices both need to write a file, either they're really one slice or the file belongs to the orchestrator.

6. Design the reducer first: five moves

(a) Give every agent a structured output contract. If results come back as prose, fan-in is a reading exercise. If they come back as typed records, fan-in is a merge. Ask each agent for the diff, the claims behind it, a confidence level and the exact files it touched.

typescript
// Every worker must return exactly this. The reducer rejects anything else.
type WorkerResult = {
  sliceId: string;
  filesTouched: string[];          // must be a subset of the slice's owned files
  diff: string;                    // unified diff
  claims: Array<{
    id: string;
    statement: string;             // 'getUser now returns null on miss instead of throwing'
    kind: 'contract-change' | 'bugfix' | 'refactor' | 'finding';
    affects: string[];             // symbols or files OUTSIDE this slice that may care
    confidence: 'high' | 'medium' | 'low';
    rationale: string;             // why: saves the reducer from rebuilding context
  }>;
  sharedFileRequests: Array<{ file: string; change: string }>; // asks, not edits
};

Two of these fields do most of the work. 'affects' makes each agent say where its change crosses into other slices, and that list feeds the seam review in (d). 'rationale' means the reducer receives the reasoning along with the diff, so it doesn't have to rebuild it.

(b) Give each shared file a single owner. Workers never edit config, shared types, route tables or barrel exports directly. They file sharedFileRequests, and the orchestrator, or one designated agent, applies them one at a time. An N-way merge conflict becomes a short, ordered queue.

(c) Deduplicate and cluster findings before anyone judges them. Group claims by the files and symbols they touch, then merge near-duplicates, and do all of it before the reducer makes a single decision. Skip this and the reducer spends expensive reasoning on three versions of the same issue. A cheap model, or plain string and symbol matching, is enough here.

(d) Add a seam agent with one narrow job: review only the boundaries between slices. Its input is every claim with a non-empty 'affects' list, plus the callers and callees on both sides. It doesn't re-review slice internals. It asks one question: does a change on one side of a boundary break an assumption on the other? This is the reviewer that would have caught the null-versus-throw bug.

text
SEAM REVIEWER
You review only cross-slice boundaries. Ignore code quality inside a slice.
For each claim with a non-empty `affects`:
  1. Load the changed symbol and every caller/consumer listed in `affects`.
  2. State the old contract and the new contract in one line each.
  3. For each consumer: does it still hold under the new contract? yes / no / unsure.
Report only 'no' and 'unsure', with file:line on both sides of the seam.

(e) Budget the reducer explicitly. Give it its own line in the plan, with a time box, a token budget and a model tier. Workers doing well-scoped generation can often run on a smaller, faster model. The judgment happens in the reducer and the seam reviewer, so that's where your strongest model belongs. If you put the strong model on the workers and treat the reducer as glue, you've spent your budget in the wrong place.

7. The rerun

I redid the same refactor with blast-radius partitioning. I used five agents instead of eight, each owning disjoint directories. The orchestrator held the shared spine, and a seam reviewer ran at the end. These numbers are again approximate and come from my own run.

  • ▹Generation got slower: about nine minutes instead of four. Bigger, coherent slices take longer than small feature slices, and the shared-file queue serialises some of the work.
  • ▹Total time dropped a lot, to roughly half an hour end to end. That compares with over an hour for the naive run, and it also beat the serial run. Most of the gain came from the reconcile tail, which shrank to about twenty minutes.
  • ▹Reducer tokens fell sharply. Claims arrived with their rationale, so the reducer stopped re-reading files to work out intent.
  • ▹The seam reviewer caught a bug in the same class as the null-versus-throw one. A pagination helper had switched from offset to cursor while a reporting consumer was still passing offsets.

Some things still went wrong:

  • ▹A shared test-fixtures file wasn't on my shared-files list. Two agents edited it, and I had a small collision to fix by hand.
  • ▹The seam reviewer raised several 'unsure' flags that turned out to be noise. That beats missing a cross-file bug, but triaging them took real minutes.
  • ▹The shared-file owner turned into a mild bottleneck. Late requests queued behind earlier ones, and one worker stalled waiting for a type it needed.

So here's the trade. You give up some of the parallel speed-up that would look good on a dashboard. In return you spend much less total time and money, and fewer bugs slip through unseen.

8. When not to fan out

One heuristic covers it: fan out only when the expected merge cost is clearly below the serial cost. Roughly:

text
parallel_cost ≈ max(slice_time) + reducer_time + seam_review_time + shared_file_queue
serial_cost   ≈ one agent, full context, doing it end to end

Fan out only if parallel_cost is clearly < serial_cost.
Reducer time grows with: number of seams × how much each seam changes.
  • ▹Tightly coupled code: if most slices touch most other slices, the seams are the whole job. Do it serially.
  • ▹Heavy shared state: schema migrations, global config overhauls and cross-cutting renames all funnel through a handful of files. Use one agent, or one agent per stage in sequence.
  • ▹Work that's mostly judgment rather than generation: architecture decisions and tricky debugging don't parallelise well. One agent with better context (the right files, docs and tests) usually beats five agents with partial context.
  • ▹Where fan-out pays off: independent modules, per-file mechanical transforms, research across separate sources, and test generation for disjoint units.

None of this is new. MapReduce, Google's 2004 framework for splitting large data jobs across many machines, taught the industry the same lesson. The map step is easy to parallelise. The pain is in the shuffle, the step that moves and groups intermediate results so they can be reduced, because that's where network, disk and stragglers hit you. A whole generation of data engineers learned to design jobs around the shuffle. People building agent systems are learning it again, with context windows and tokens playing the part of network and disk.

9. Checklist: tape this to your monitor

text
BEFORE YOU SPAWN N AGENTS
[ ] 1. Draw the dependency graph (modules, shared files, interfaces)
[ ] 2. Mark shared files; assign ONE owner (orchestrator or one agent)
[ ] 3. Define the output contract: diff + claims + confidence + files touched
[ ] 4. Name the seams between slices and who reviews them
[ ] 5. Set a reducer budget: time, tokens, model tier
[ ] 6. Estimate merge cost vs serial cost. If merge >= serial, don't fan out.

10. Tomorrow: fan-out on purpose, fan-in as a fight

Today, fan-in meant reconciling different slices of one job. On Day 24 we flip it. We'll fan out the same task deliberately, either as N independent attempts or as the same code reviewed through different lenses, and make fan-in adversarial: a verifier picks a winner or refutes every candidate. Used that way, redundancy is a tool for correctness rather than waste.

An exercise before then: take your last multi-agent run and list every bug or conflict that showed up after the merge. For each one, ask whether a different partition would have put both sides of the problem in one agent's context. If the answer is usually yes, the partition caused those problems, not the agents.

Flashcards
Check yourself

Extend your knowledge

  • ▹Read 'MapReduce: Simplified Data Processing on Large Clusters' by Jeffrey Dean and Sanjay Ghemawat (OSDI 2004). Focus on the shuffle and stragglers, and map each problem onto a step in multi-agent fan-in.
  • ▹Read Anthropic's engineering posts 'Building effective agents' (the parallelization and orchestrator-workers patterns) and 'How we built our multi-agent research system'. Note where they spend effort on the merge and on the lead agent.
  • ▹Read Melvin Conway's 'How Do Committees Invent?' (1968). Then sketch your current agent topology next to your codebase's module boundaries and mark where the two disagree.
  • ▹Exercise: on your next Claude Code subagent or workflow run, add a WorkerResult-style contract and a seam-reviewer pass. Log the reducer's tokens and minutes separately from the workers'.
Test yourself on this lesson →

Discussion

Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.

Ask me anything about “Fan-out and fan-in” — trade-offs, decisions, or the story behind it.