Back to blog
Series · Day 30
Distributed Systems in 30 Days
View all lessons →
Distributed computing fallacies for agents

Day 30: Multi-agent LLM systems are distributed systems, and they also add a ninth fallacy

Your agent framework is hiding a network from you, and it does that on purpose. Every tool call and every handoff between agents crosses a boundary you can't see, because the framework's job is to make it disappear. Ship the workflow as if every call were local and production load will show you the mistake: duplicate writes, token bills nobody budgeted for, and actions that ran without errors and still did the wrong thing.

1. Hook: the plan that died at step 37

A planner agent writes a 40-step plan to move customer accounts onto a new billing system, and the executor starts working through it. Step 12 creates invoice records. At step 37 a CRM call takes 31 seconds against a 30-second framework timeout, so the framework throws. The orchestrator catches the exception and does the obvious thing: it retries the run from step 1. Steps 1 to 36 run again, and step 12 creates every invoice a second time. Step 37 had actually succeeded on the server the first time. Only the response was late. No alert fires and the run finishes green. Finance finds the duplicates three weeks later.

Here's the line that let it happen. I haven't taken it from any one framework, because most of them look like this:

python
@tool
def create_invoice(customer_id: str, amount: float) -> dict:
    return billing_api.post('/invoices', json={'customer_id': customer_id, 'amount': amount})

# The model emits a tool call; the framework dispatches it.
# From the planner's point of view, this is just a function that returns a dict.

The decorator is the problem. It dresses a network request to a third-party system up as a local function. A local function can't half-succeed on someone else's server. It can't time out while the work quietly finishes somewhere else. You can replay it without creating duplicate records. This call can do all three of those things. Here's the same tool after applying what the earlier days of this series covered:

python
@tool
def create_invoice(customer_id: str, amount_cents: int, *, ctx: RunContext) -> dict:
    # Checkpoint: a retried run skips work that already committed
    if ctx.checkpoint.done(ctx.step_id):
        return ctx.checkpoint.result(ctx.step_id)

    key = f'{ctx.run_id}:{ctx.step_id}'          # stable across retries
    resp = billing_api.post(
        '/invoices',
        json={'customer_id': customer_id, 'amount_cents': amount_cents},
        headers={'Idempotency-Key': key},         # server dedupes a replay
        timeout=ctx.deadline.remaining(),         # share of the run's budget, not a fixed 30s
    )
    ctx.checkpoint.save(ctx.step_id, resp)
    return resp

2. The 1994 memo in 90 seconds

Around 1994, Peter Deutsch at Sun Microsystems wrote down seven false assumptions that engineers new to distributed computing keep making. The list usually carries his name, though some items were already going around Sun before then, and James Gosling later added an eighth. Here they are: the network is reliable, latency is zero, bandwidth is infinite, the network is secure, topology doesn't change, there is one administrator, transport cost is zero, and the network is homogeneous. That same year a paper from Sun Labs, Waldo et al., 'A Note on Distributed Computing', argued directly against unifying local and remote objects. You can't make a remote call look like a local one, because hiding partial failure, latency and concurrency behind an interface doesn't make them go away. Agent frameworks repeat that mistake on purpose, because it makes them nicer to program against.

3. The mapping: eight cards

Card 1: The network is reliable

  • ▹Agent-era version: the tool call returns.
  • ▹Failure: the call times out after the server-side write has already committed, and the retry writes again. That's the step 37 story.
  • ▹Series fix: retries with backoff, always paired with idempotency keys (the Stripe-style Idempotency-Key header) and checkpointed steps, so a retry picks up where it stopped instead of starting from scratch.

Card 2: Latency is zero

  • ▹Agent-era version: the sub-agent answers in one turn.
  • ▹Failure: a researcher sub-agent spins through 14 tool calls while the user-facing request waits. The request hits a gateway timeout, and the parent agent never finds out why.
  • ▹Series fix: timeouts and deadline budgets that propagate down the call tree, the way gRPC deadlines are supposed to. Each hop gets whatever is left of its parent's budget, not a fresh default.

Card 3: Bandwidth is infinite

  • ▹Agent-era version: the context window holds everything.
  • ▹Failure: the orchestrator hands the full transcript to every sub-agent. Key instructions sink into a long context, and the handoff summary drops the one constraint that mattered.
  • ▹Series fix: backpressure and deliberate payload design. Pass references, not blobs. Pass structured state, not transcript dumps. Treat context as a constrained link.

Card 4: Transport cost is zero

  • ▹Agent-era version: tokens are free.
  • ▹Failure: agents calling agents multiply spend, just as chatty microservices once multiplied egress bills. Anthropic's write-up on its multi-agent research system reported that multi-agent runs used roughly 15 times the tokens of a chat interaction. Their point was that the cost only pays off on tasks valuable enough to justify it.
  • ▹Series fix: per-request cost budgets, caps on fan-out, and load shedding, so the system degrades gracefully when it hits a limit instead of spending without one.

Card 5: Topology doesn't change

  • ▹Agent-era version: the tool list and the model version are fixed.
  • ▹Failure: an MCP server adds a tool with a similar name, or the provider moves a model alias, and the planner starts routing to a different tool. You didn't change a line of code.
  • ▹Series fix: service discovery with versioning. Pin model versions, version your tool manifests, and review a manifest change the way you'd review a schema migration.

Card 6: There is one administrator

  • ▹Agent-era version: one system prompt governs every agent.
  • ▹Failure: the orchestrator's prompt says 'never touch production'. The third-party coding sub-agent runs on its own prompt, owned by another team, and that rule never reached it.
  • ▹Series fix: clear ownership and policy enforced at the boundary. Put constraints in the tool layer (permissions, scoped credentials), not in prose you hope gets forwarded.

Card 7: The network is secure

  • ▹Agent-era version: tool output is trusted input.
  • ▹Failure: a web page or support ticket that a tool fetched contains instructions, and the agent follows them. Prompt injection is an untrusted network delivering data that the agent treats as instructions. It sits at number one on the OWASP Top 10 for LLM Applications. For a threat model, use Simon Willison's 'lethal trifecta': private data, untrusted content and a way to communicate externally, all present at the same time.
  • ▹Series fix: trust boundaries. Mark tool output as data, keep privileged actions out of reach of any context that has seen untrusted content, and put an approval step in front of the high-risk sinks.

Card 8: The network is homogeneous

  • ▹Agent-era version: every model reads instructions the same way.
  • ▹Failure: a prompt tuned on one model family gets sent to a cheaper model for a sub-task. The cheaper model treats 'should' as optional and skips validation.
  • ▹Series fix: contract tests at every interface. Run the same handoff fixtures against every model that could sit behind it, the way you'd test clients in different languages against one API.

4. Evidence: more than an analogy

The best-known empirical work on this is 'Why Do Multi-Agent LLM Systems Fail?' (Cemri et al., UC Berkeley, 2025), which introduced the MAST taxonomy. The authors annotated execution traces from several open-source multi-agent frameworks, including MetaGPT, ChatDev, AG2 and Magentic-One. They sorted the failures into 14 modes under three headings: specification and system design, inter-agent misalignment, and task verification and termination. The finding I take from it is that a lot of failures come from how the system is organised, not just from what the underlying model can't do. The mapping below, from their modes onto the fallacies, is mine. The paper doesn't make it.

  • ▹Clean matches to classic distributed-systems failures: step repetition (retries without idempotency), loss of conversation history (lost state with no durable checkpoint), conversation reset (crash and restart with no recovery), unaware of termination conditions and premature termination (no deadline or termination protocol), and arguably information withholding (lossy payloads, which is the bandwidth fallacy).
  • ▹No match to any of the eight: disobeying the task specification, disobeying the role specification, failing to ask for clarification, task derailment, ignoring another agent's input, reasoning-action mismatch, no or incomplete verification, and incorrect verification.
  • ▹By my count, about 5 or 6 of the 14 are distributed-systems failures that Days 1 to 29 already cover. The remaining 8 or so are failures of meaning.

There's another side to this, and it matters. Communication between agents is also the whole reason to build a multi-agent system. The multi-agent RL survey by Zhu et al. (arXiv 2203.08975) treats communication as a way to coordinate better. It proposes nine dimensions for analysing communication design, because the field had no systematic way to classify approaches, and that backs up the idea that every message is a protocol decision. The α-UMi work (arXiv 2401.07324) splits tool use across separate planner, caller and summariser LLMs and reports better results than a single LLM on tool-use benchmarks. So distribution is a trade-off. Every split buys capability and also adds another handoff that can fail. My rule is to add an agent boundary only when what it buys is worth one more remote call. For the cautious side of that argument, Cognition's essay 'Don't Build Multi-Agents' makes the case well.

5. The leftover column: fallacy #9

Fallacy #9: the other node understands what I meant. In a classic distributed system, once a message arrives intact and parses against the schema, code decides how the receiver interprets it. In an agent system the receiver is a model reading natural language, so a message can be delivered perfectly and still be misread. TCP won't catch that. Neither will schemas, retries or consensus.

Here's a concrete case. The planner sends {'task': 'clean up the old records in crm.contacts'}, meaning archive anything nobody has touched since 2023. The executor takes 'clean up' to mean delete, chooses its own cutoff, and runs DELETE on 4,212 rows. The JSON was valid and the tool call succeeded. Every transport-level guarantee held, and the data is still gone.

This has happened outside LLMs too. In cooperative Hanabi research, a hint can arrive perfectly and still tell the other player very little, unless both players share a convention for what it means. One recent paper (arXiv 2412.06333) added conventions drawn from human play to the agents' action space and saw better performance in both self-play and cross-play, where agents work with partners they never trained with. Shared meaning there behaves like a small agreement protocol between players. You won't find it in any single message. The α-UMi paper also points out that its modular design lets you update each role independently. A distributed-systems engineer would call that version skew: an updated planner and an older caller can quietly stop agreeing on what a message means. The paper doesn't raise that risk. That part is my inference.

6. Why the old toolkit only gets you halfway on #9

  • ▹Schemas check structure, not meaning. {'action': 'clean up'} passes any schema that types action as a string.
  • ▹Retries repeat the misreading. When the instruction is ambiguous, the second attempt usually resolves it the same way as the first. All you've done is make the mistake idempotent.
  • ▹Consensus among copies of one model is weaker than it looks. Three instances of the same model voting on what 'clean up' means are correlated replicas, not independent ones, and they tend to lean the same way.

What does work is four techniques that check meaning, not just format:

  • ▹Typed contracts between agents. Take free-text verbs out of every handoff that has side effects. Give the planner a closed enum such as ARCHIVE, SOFT_DELETE or HARD_DELETE and make it choose one.
  • ▹Read-back confirmation. Pilots read clearances back to the controller, and the controller corrects anything that doesn't match. Before any side effect, the executor says exactly what it's about to do, with real numbers, and the planner (or a human) checks that against the intent.
  • ▹Invariants at every hop. Check things that have to be true however the message was read. The action must be reversible unless explicitly allowed. Row counts must land within a tolerance of the estimate. No writes outside the scoped tables.
  • ▹Verification against intent, not format. Hand the verifier the original human goal word for word, not the executor's summary of it, and have it check the result against that. The pipeline in arXiv 2508.08322 puts a dedicated Intent Translator in front of its sub-agents for much the same reason: someone has to engineer 'what was meant' explicitly.
python
class CleanupAction(str, Enum):
    ARCHIVE = 'archive'
    SOFT_DELETE = 'soft_delete'
    HARD_DELETE = 'hard_delete'

class CleanupCommand(BaseModel):
    intent: str                 # original human goal, carried verbatim to every hop
    action: CleanupAction       # no free-text verbs
    table: str
    where: str
    expected_rows: int          # planner's estimate, used as an invariant
    allow_irreversible: bool = False

def execute(cmd: CleanupCommand, db, planner) -> Result:
    if cmd.action == CleanupAction.HARD_DELETE and not cmd.allow_irreversible:
        raise InvariantViolation('irreversible action not authorised')

    actual = db.count(cmd.table, cmd.where)
    if abs(actual - cmd.expected_rows) > 0.1 * max(cmd.expected_rows, 1):
        raise InvariantViolation(f'expected ~{cmd.expected_rows} rows, found {actual}')

    readback = f'I will {cmd.action.value} {actual} rows in {cmd.table} where {cmd.where}'
    if not planner.confirm(readback, against=cmd.intent):   # read-back, checked against intent
        raise Rejected(readback)

    return db.apply(cmd)

7. The agent fallacy checklist

Work through this before you ship any multi-agent workflow. Any item you can't answer probably points to a fallacy your system still has.

  • ▹Checkpoints: if a run fails, can it resume from the last committed step, or does it go back to step 1?
  • ▹Idempotency keys: does every side-effecting tool call send a key that survives retries (run_id plus step_id)? Does the downstream system actually honour it?
  • ▹Deadline budgets: does each hop get its parent's remaining budget, and is there a hard ceiling on the user-facing request?
  • ▹Token budgets: is there a cost ceiling per request and a cap on fan-out? What happens when you hit either one?
  • ▹Version pinning: are model versions and tool manifests pinned, with changes reviewed like schema migrations?
  • ▹Policy in the tool layer: are permissions enforced with scoped credentials, or written into a prompt that some agents never see?
  • ▹Trust boundary: is tool output handled as data? Can a context that has read untrusted content still reach a privileged action?
  • ▹Cross-model contract tests: do the handoff fixtures pass on every model that might sit behind each interface?
  • ▹Typed handoffs: is there still a free-text verb in any handoff that has side effects?
  • ▹Read-back: before every irreversible action, does the executor state the concrete plan, and does someone check it against the intent?
  • ▹Invariants: at each hop, what gets checked that has to hold however the message was interpreted?
  • ▹Intent verification: does the final check compare the result against the original human goal, word for word?

When to skip all this: if one agent with a handful of tools can do the job, don't split it. Every boundary you don't add means a few fallacies you never have to guard against.

8. Course close

These 30 days were never really about networks. They were about one assumption: anything you can't see is a remote call. It can be slow, fail halfway, run twice, or belong to someone else. Timeouts, retries, idempotency, handling partial failure and consensus are all ways of building for that. For most of us, agents are now the biggest remote call we'll ever make, and they add a failure mode the old list doesn't cover: the receiver can misunderstand you. Build for both.

Step 37 never failed. It was a remote call that nobody treated as remote.

Flashcards
Check yourself

Extend your knowledge

  • ▹Read 'Why Do Multi-Agent LLM Systems Fail?' (Cemri et al., 2025). Then go through the MAST failure modes and mark each one 'transport' or 'meaning' for your own system.
  • ▹Read Waldo, Wyant, Wollrath and Kendall, 'A Note on Distributed Computing' (Sun Microsystems Laboratories, 1994). It's short, and it's still the clearest explanation of why a remote call can't be made to look local.
  • ▹Read Anthropic's engineering post on building its multi-agent research system, then Cognition's 'Don't Build Multi-Agents'. Between them you get both sides of the decomposition trade-off.
  • ▹Exercise: choose one side-effecting tool in your agent stack. Give it an idempotency key, a deadline taken from the parent's budget, and a read-back step before it runs. Then replay a production trace with a timeout injected at the last step and see what happens.
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 “Distributed computing fallacies for agents” — trade-offs, decisions, or the story behind it.