Home  /  Articles  /  Pub/Sub vs. Role-Based Coordination for Multi-Agent LLM Systems: What I Run in Production
11 Multi Agent Systemsminute read

Pub/Sub vs. Role-Based Coordination for Multi-Agent LLM Systems: What I Run in Production

I went back through three weeks of logs from a multi-agent LLM system my team had wired together for a midsize backend refactor, and the number that stopped me was this: 61 percent of our token spend…

A photograph of a software engineer seated at a minimalist desk in a softly…

I went back through three weeks of logs from a multi-agent LLM system my team had wired together for a midsize backend refactor, and the number that stopped me was this: 61 percent of our token spend went to coordination, not to writing or reviewing code. Agents were re-reading each other's full conversation history to figure out who had touched what. The actual work — generating a migration, patching a handler, writing a test — was the cheap part. The expensive part was every agent trying to stay aware of every other agent.

That ratio is the whole story. If you are building multi-agent LLM systems for real software development and you have hit a wall somewhere past three or four agents, the wall is almost always coordination overhead, and the cause is almost always that your agents are organized around who they pretend to be instead of what part of the system they own.

Here is the verdict up front, the way I wish someone had handed it to me: for agents doing real engineering work across a codebase, route messages by service boundary over a publish-subscribe bus, not by simulated organizational role. The service graph you already have is a better coordination topology than any "CEO, CTO, programmer, tester" hierarchy you can invent.

I'll lay this out in three parts: what most teams do, what the behavior of these systems actually tells us, and what I run now after watching the first two approaches fail in different ways. No affiliate links here, nothing to sell — this is a comparison of patterns, and the patterns are free.

What most people do

If you came up through ChatDev or MetaGPT, you inherited a specific mental model: a software company, staffed by agents, each playing a job title. There's a product manager agent that writes requirements, an architect agent that produces a design, one or more programmer agents that implement, a reviewer or tester agent that checks the output. Messages flow along the org chart. The output of one role becomes the input of the next.

This is a genuinely good idea for a demo, and I want to be fair about why. It maps onto something humans already understand — a software team — so it's legible. It produces coherent artifacts for small, self-contained projects: build me a snake game, build me a todo app. The role pipeline gives you a natural sequence, which means you get a natural place to insert review gates. For a single deliverable that fits in a few files, it works.

The coordination underneath it almost always takes one of two shapes, and both of them are where it breaks.

The first shape is broadcast-to-all. Every agent shares one conversation, one scratchpad, one running transcript. When the programmer agent makes a change, it's appended to the shared context, and every other agent sees the whole thing on its next turn. This is simple to build and it is a disaster at scale, because the context each agent must read grows with the total activity of the system, not with the work that agent actually cares about. Ten agents producing modest output will bury each other. You pay tokens for it on every single turn, and you pay latency for it, and worst of all you pay in degraded reasoning — models attend worse to relevant detail when it's drowning in irrelevant detail. That's the 61 percent.

The second shape is the transcript as state. Instead of maintaining a structured representation of the codebase, the system treats the chat history as the source of truth about what exists. Need to know the current shape of the OrderService API? Scroll back through the conversation until you find where someone last described it. This is fine for fifty messages. At five thousand, agents start contradicting decisions made earlier because the relevant message has fallen out of the window, and you get the multi-agent equivalent of two engineers who never talk merging conflicting changes.

Both shapes share the same root assumption: that the unit of coordination is the role. The architect hands off to the programmer hands off to the tester. But real systems are not organized by role. A backend with an auth service, a billing service, a notification service, and an API gateway is organized by bounded context. Two engineers can work in parallel for weeks without coordinating, as long as they own different services and the contract between them holds. The role-based frameworks throw that parallelism away. They serialize work that doesn't need to be serialized, and they couple agents that have no business being coupled.

I don't think this was a mistake by the people who built those frameworks. They were solving "can a group of LLMs produce a working program at all," and the answer turned out to be yes. The org-chart metaphor was a reasonable first primitive. It just doesn't survive contact with a codebase that has more than one moving part.

What the evidence suggests

When I say evidence I don't mean a study — I mean the observable behavior of these systems under load, which is the only evidence that matters when you're the one paying the bill.

Three things show up consistently once you run multi-agent setups against a non-trivial repository.

A wide-angle photograph of a dimly lit modern engineering office at night, multiple large…

First: coordination cost is quadratic when you broadcast and linear when you scope. With N agents on a shared context, every agent reads every other agent's output, so total context handling scales with N². With agents scoped to subscriptions, an agent only reads what's relevant to its boundary, and the cost scales with the actual dependency fan-out, which in a well-factored system is small and roughly constant. This is not a subtle effect. Going from broadcast to scoped subscription on that backend refactor cut our coordination tokens by more than half without changing a single prompt.

Second: the context window is the real constraint, and you should treat it like memory pressure. Everyone talks about context windows getting bigger, but a bigger window doesn't fix the attention-dilution problem and it doesn't fix the cost problem — a 200K-token window filled with 200K tokens of other agents' chatter is more expensive and reasons worse than a 20K-token window filled with exactly the right 20K. The discipline that matters is the same discipline that matters in distributed systems generally: minimize the surface area each component has to know about. Bounded context is that discipline.

Third: state has to live somewhere structured, not in the conversation. The moment you have more than one agent, the codebase's current shape needs a representation that survives the context window. You need something queryable — give me the current interface of the billing service, give me what changed in auth since version 14 — that doesn't require replaying history.

Put those three together and the topology designs itself. You want a publish-subscribe bus keyed to the service graph. Each service in your repo is a topic. An agent owns one or more services, subscribes to its own topics and to the topics of services it depends on, and ignores everything else. When an agent changes a service, it publishes a structured event. Only the subscribers wake up.

Here's what one of those events actually looks like in the system I run now:

{
  "topic": "service.billing",
  "version": 18,
  "previous_version": 17,
  "author_agent": "agent-billing-01",
  "change_kind": "interface",
  "summary": "Added idempotency_key to POST /charges; refunds now async",
  "diff": "@@ POST /charges @@\n+  idempotency_key: string (required)\n@@ POST /refunds @@\n-  returns: RefundResult\n+  returns: RefundJobHandle",
  "affected_consumers": ["service.api-gateway", "service.notifications"]
}

The notification agent and the gateway agent are subscribed to service.billing because their manifests declared a dependency. They get this event. They get the diff, not the whole conversation that produced it, and not the entire current state of billing — just what changed and a one-line summary of why. The auth agent never hears about it, because auth doesn't consume billing. That's the entire trick. The coordination cost for any given change is proportional to how many things actually depend on the thing that changed.

The other half is versioning. Every topic carries a monotonically increasing version. The full current state of each service lives in a versioned document store, separate from the message bus. An agent that joins late, or one whose context got reset, doesn't replay history — it reads the current version of the documents for the services it owns and the contracts of the services it depends on. The bus carries deltas; the store carries truth. That split is the same one you'd find in any event-sourced system, and it's load-bearing here for the same reason: it decouples "staying current" from "remembering everything."

On transport: if your agents already speak MCP, you have most of what you need. MCP gives you a clean way to expose the document store and the subscription interface as tools the agent can call — read the current spec, publish a change, list pending deltas on my topics. You are not inventing a protocol. You're using the one your agents already have to talk to a coordination layer instead of talking directly to each other. The point is that the bus is a service the agents consult, not a chat room they all sit in.

There's a payload-design subtlety worth naming. The summary and the diff are both there on purpose. The diff is precise but expensive to read and easy to misread out of context. The summary is cheap and survives compression. A subscribed agent decides, based on change_kind and affected_consumers, whether this delta touches its own contract — and only then pulls and reads the full diff. Most deltas an agent hears about don't require it to do anything. The summary lets it make that call without spending the tokens to read the diff. This is the same instinct as a cache header: tell the consumer enough to decide whether it needs the expensive thing.

What I actually do

The setup I run now has four parts, and I'll be honest about the cost of each because none of them is free.

A service manifest per agent. Before anything runs, each agent gets a manifest: which services it owns, which it depends on, what its review obligations are. This maps directly to the repo's actual structure — for a monorepo, it's roughly your package graph; for separate repos, it's your dependency declarations. I generate the first draft of these manifests by parsing the existing build files and import graph, then correct by hand. The honest cost: if your service boundaries are mushy — if everything imports everything, if you have a god-module — this step is painful, because it surfaces architectural debt you'd been ignoring. That pain is information, but it's still pain, and it lands before you get any value.

A close-up macro photograph of a complex network topology visualized on a sleek display…

A pub/sub bus with topics per service. Agents subscribe according to their manifests. I've run this on plain Redis pub/sub for prototypes and on a real broker for anything I trusted. The bus only ever carries deltas — the structured events shown above. The honest cost: this is eventually consistent. An agent can act on version 17 of a contract while version 18 is in flight. You need to handle the stale-read case — version checks on publish, a re-read when a publish is rejected as out of date — and that's real engineering you have to do, not a thing the pattern hands you.

A versioned document store as source of truth. Current interface specs, current schemas, current architectural decisions, each versioned per service. Agents read truth from here, not from history. This is also what makes onboarding cheap: a new agent, or a recovered one, reads the current docs for its boundary and is current, without replaying anything. The honest cost: keeping the store honest requires that publishing a change and updating the store be one atomic-ish operation, or your truth and your deltas drift apart. I enforce it at the tool layer — the publish tool writes the store and emits the event together — but if an agent crashes between the two, you have reconciliation to do.

Diff-scoped notifications with summary-first reads. Covered above. The thing I'll add: the biggest win wasn't the token savings, it was the failure modes getting legible. When the gateway agent broke, I could see exactly which billing delta it had consumed and how it had interpreted it, because the interaction was a discrete, logged, scoped event — not a needle in a shared transcript.

Here's how I'd actually decide between this and a role-based framework, by criterion:

Criterion Role-based (ChatDev/MetaGPT lineage) Service-boundary pub/sub
Best at Small, single-artifact projects; legible linear pipelines Multi-service codebases with real parallelism
Coordination cost Grows with total activity (≈N²) Grows with dependency fan-out (≈constant)
State model Conversation transcript Versioned document store + delta bus
Setup cost Low — pick roles, go High — manifests, bus, store, version handling
Debugging Hard — scattered through transcript Easier — scoped, logged events
Wrong for Anything past ~4 agents on a real repo Tiny projects; one-shot generation; fuzzy boundaries
Worst failure Context bloat, contradicted decisions Stale reads, store/bus drift

The honest negative on my own approach, stated plainly: it has a high floor. If your task is "generate one small program," the role pipeline will beat me on time-to-first-result and you should use it. The pub/sub topology earns its keep only when you have genuine service boundaries and enough parallel work that the coordination cost would otherwise dominate. If you bolt this onto a codebase whose boundaries are fictional, you'll spend a week building infrastructure to coordinate agents that all need to know everything anyway, and you'll have been better off with the broadcast you were trying to escape.

Who this is for

You, if: you have a real service graph or a monorepo with clean package boundaries, you're running enough agents that coordination cost is visibly on your bill, your agents already speak MCP or can, and you've felt the specific frustration of role pipelines serializing work that should run in parallel.

Who this isn't for

You, if: you're generating single, small artifacts; you're at the proof-of-concept stage where time-to-first-result matters more than scaling cost; or — and be honest here — your codebase doesn't actually have boundaries, in which case fix that first, because no coordination layer will save agents from an architecture that couples everything to everything.

If I had to compress the whole comparison into one line: role-based frameworks coordinate agents around a metaphor, and service-boundary pub/sub coordinates them around the structure you already have, which is why it costs less to run and breaks in more legible ways.

But I don't want to hand you a clean resolution, because the part I'm least sure about is the part that decides whether this actually scales to large systems. I built the notification layer on the assumption that a subscribed agent can read a one-line summary, decide most deltas don't concern it, and skip reading the full diff. That works when the summary faithfully signals relevance. The open question — and I don't have an answer I trust — is whether agents can subscribe to summaries instead of diffs as a matter of course, compressing the bus even further, without losing the signal that turns out to matter. Sometimes the thing that breaks the gateway is a one-character change in a diff that no reasonable summary would have flagged. How aggressively can you compress what an agent hears about a change before you've quietly removed the information it needed to do its job? I genuinely don't know where that line is, and I suspect it moves with the model.

Multi Agent Systems Llm Tooling Architecture