DEV Community

Cover image for Orchestrating 5 Agents in LangGraph Without Deadlocks
Naman Tiwari
Naman Tiwari

Posted on

Orchestrating 5 Agents in LangGraph Without Deadlocks

The first version of my Multi-Agent Collaborative Development System had five specialized agents, a shared state object, and a graph that let any agent hand off to any other agent based on its own judgment. It looked flexible on the whiteboard. In practice, it occasionally just... stopped. Two agents would hand control back and forth to each other, each one waiting for the other to produce something it needed, and the run would sit there burning nothing but time until I killed the process.

That was my introduction to the fact that multi-agent orchestration has all the classic distributed-systems problems — deadlock, livelock, unbounded loops — except now the "processes" are non-deterministic LLM calls instead of well-behaved threads. Here's how I redesigned the system to make those failure modes structurally impossible instead of just "unlikely."

Why free-form hand-off is the problem, not the feature

The instinct when building a multi-agent system is to make it feel autonomous: let each agent decide who should act next based on the current state. It's appealing because it mirrors how a real engineering team works — nobody's following a rigid script, people just pick up the next relevant task.

The problem is that LLM agents don't have the social context a human team has. A human knows when they're stuck and escalates. An LLM agent, asked "who should act next," will confidently hand off to another agent even when nothing has actually changed in the state — which is exactly how you get two agents lobbing the same unresolved task back and forth forever. Free-form routing turns your control flow into a graph where cycles are not just possible but likely, and LLMs are bad at noticing they're in one.

The fix wasn't "make the agents smarter." It was taking the routing decision away from open-ended agent judgment and putting structure around it.

Orchestrator-with-sub-graphs, not agent-to-agent hand-off

The architecture that actually worked is a router-hub pattern: a central orchestrator node owns all control-flow decisions, and the five specialized agents never talk to each other directly. Each agent is a sub-graph that gets invoked by the orchestrator, does its job, and returns control to the orchestrator — never to another agent.

This one change removes an entire category of deadlock. If agent A can never hand off directly to agent B, then A and B can never get stuck in a two-node cycle waiting on each other. Every path through the system goes agent → orchestrator → agent, which means the orchestrator is the only place a cycle could ever form, and that's exactly where you want a single, inspectable chokepoint for your control logic.

Practically, this means the LangGraph structure looks less like a mesh and more like a star, with the orchestrator as the hub and the five agents as spokes. The orchestrator reads the current state, decides which spoke needs to fire next (or whether the run is done), and dispatches.

Turn-capping: the deadlock breaker of last resort

Structure prevents most cycles, but not all of them — an orchestrator that keeps deciding "agent needs another pass" because a task genuinely isn't converging is a real scenario, not a bug. For that case, the system needs a backstop that doesn't rely on the LLM correctly recognizing it's stuck.

I added a turn-capped loop: every agent invocation increments a counter in shared state, and the orchestrator has a hard ceiling on total turns before it's forced to either escalate to a human-in-the-loop checkpoint or terminate the run with a clear failure state. This is the distributed-systems equivalent of a TTL on a packet — it doesn't make the system smarter, it just guarantees termination. A system that fails loudly at turn 20 is infinitely more debuggable than one that spins silently until you notice the process is still running an hour later.

The cap number matters less than having one at all. I'd rather have a run terminate one iteration too early with an explicit "did not converge" state than have it run forever with no defined endpoint.

Human-in-the-loop as a state transition, not an escape hatch

The other piece that mattered: human validation checkpoints aren't bolted on as a side-channel, they're modeled as an explicit node in the graph, with defined entry and exit conditions. When the orchestrator routes to the human-checkpoint node, execution genuinely pauses — the graph waits for external input before any agent can act again, rather than the orchestrator trying to "guess" what a human would approve and proceeding anyway.

This matters for deadlock prevention in a way that isn't obvious at first: making the human checkpoint a real state (not a suggestion the orchestrator can ignore) means it doubles as a manual override for exactly the kind of stuck states the turn cap is meant to catch automatically. If something's gone wrong in a way I didn't anticipate, a human can inspect the shared state at the checkpoint and redirect — which is a much better failure mode than the system quietly looping.

Shared state as the single source of truth

None of the above works if agents are passing partial context to each other directly instead of reading from and writing to one shared state object. Early on, some of my agents' prompts included hand-crafted summaries of "what the previous agent said," which is a subtle way of smuggling agent-to-agent coupling back into a system that's supposed to be hub-and-spoke. I moved everything — task status, intermediate outputs, validation results, turn count — into a single typed state object that every node reads from and writes to. The orchestrator's routing decision is then a pure function of that state, which makes it possible to actually reason about (and test) what the orchestrator will do next, instead of it being an emergent property of whatever text happened to get passed around.

What this actually bought me

  • No two-agent cycles are structurally possible, because agents don't hand off to each other.
  • Every run terminates, because the turn cap guarantees an upper bound regardless of what the orchestrator decides.
  • Stuck states are recoverable, because human checkpoints are real graph nodes with the authority to redirect, not passive log lines.
  • The system is debuggable, because the orchestrator's decision is a function of one inspectable state object, not a black box of inter-agent messages.

The broader lesson

Deadlocks in multi-agent LLM systems aren't a language-model problem, they're a control-flow architecture problem wearing an AI costume. The fix isn't better prompting or a smarter router agent — it's borrowing the same discipline distributed systems have used for decades: centralize control flow, bound your loops, and make your failure modes explicit states instead of implicit hopes. Five agents that can only talk through a hub, with a hard turn cap and a real human checkpoint, will reliably converge or reliably fail loudly. Five agents that can talk to each other freely will occasionally just disappear into the graph.

Top comments (1)

Collapse
 
swapnoneel123 profile image
Swapnoneel Saha

the hub and hard turn cap make the control flow easier to reason about. i would add a per node timeout and an idempotency key for each state transition, because a retry can repeat a tool call after the worker finished but before the result was stored. a small trace with state version, selected agent, turn count, and reason for the next edge would also make livelock and stale writes easier to find. these checks keep the single state object reliable under failure.