How to Fix 'agent infinite loop when scaling' in LangGraph (TypeScript)
When LangGraph starts looping forever during scale-out, it usually means your graph never reaches a terminal state or keeps re-triggering the same node with the same state. In TypeScript, this often shows up after adding retries, multi-agent routing, or a conditional edge that accidentally points back to itself.
The symptom is usually one of these:
- •
GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition - •repeated execution of the same node in your logs
- •workers spinning until timeout when you deploy multiple instances
The Most Common Cause
The #1 cause is a routing function that never returns a valid stop path, so the graph keeps sending control back into the loop. This gets worse under scaling because concurrent runs amplify the bad transition logic.
Here’s the broken pattern and the fixed pattern side by side.
| Broken | Fixed |
|---|---|
| ```ts | |
| import { StateGraph, END } from "@langchain/langgraph"; |
type State = { messages: string[]; done?: boolean; };
const graph = new StateGraph<State>();
graph.addNode("agent", async (state) => { const nextMessage = await callModel(state.messages); return { messages: [...state.messages, nextMessage], done: nextMessage.includes("finished"), }; });
graph.addConditionalEdges("agent", (state) => { // BUG: never returns END return "agent"; });
graph.setEntryPoint("agent");
|ts
import { StateGraph, END } from "@langchain/langgraph";
type State = { messages: string[]; done?: boolean; };
const graph = new StateGraph<State>();
graph.addNode("agent", async (state) => { const nextMessage = await callModel(state.messages); return { messages: [...state.messages, nextMessage], done: nextMessage.includes("finished"), }; });
graph.addConditionalEdges("agent", (state) => { return state.done ? END : "agent"; });
graph.setEntryPoint("agent");
The key rule: every loop needs an explicit exit path. If your conditional edge always returns the same node, LangGraph will keep executing until it hits `GraphRecursionError`.
## Other Possible Causes
### 1. State is not changing between iterations
If your node returns the same state every time, LangGraph has no reason to progress.
```ts
graph.addNode("summarize", async (state) => {
return {
...state,
summary: state.summary, // no change
};
});
Fix it by writing a real update:
graph.addNode("summarize", async (state) => {
const summary = await summarizeMessages(state.messages);
return { ...state, summary };
});
2. Conditional routing checks the wrong field
This happens when your router reads stale or unrelated data.
graph.addConditionalEdges("router", (state) => {
// BUG: checking input flag instead of updated state
return state.input?.shouldContinue ? "worker" : END;
});
Use the field your nodes actually mutate:
graph.addConditionalEdges("router", (state) => {
return state.done ? END : "worker";
});
3. Parallel branches rejoin into a loop
With scaling, multiple branches can all re-enter the same node if you wire joins incorrectly.
graph.addEdge("extractor", "classifier");
graph.addEdge("classifier", "extractor"); // accidental cycle
Break the cycle with a terminal node or a guard:
graph.addEdge("extractor", "classifier");
graph.addConditionalEdges("classifier", (state) =>
state.needsMoreWork ? "extractor" : END
);
4. Retry logic replays the same failing step forever
If you wrap a node in custom retry logic and never surface failure, the graph can keep bouncing.
graph.addNode("fetchPolicy", async (state) => {
try {
return await fetchPolicyData(state.policyId);
} catch {
return state; // BUG: hides failure and preserves loop conditions
}
});
Return an error flag and route out:
graph.addNode("fetchPolicy", async (state) => {
try {
const policy = await fetchPolicyData(state.policyId);
return { ...state, policy, fetchFailed: false };
} catch (error) {
return { ...state, fetchFailed: true };
}
});
graph.addConditionalEdges("fetchPolicy", (state) =>
state.fetchFailed ? END : "nextStep"
);
How to Debug It
- •
Turn on step logging
- •Log node name, state keys, and routing decision on every transition.
- •You want to see exactly which node repeats.
graph.addConditionalEdges("agent", (state) => { console.log({ node: "agent", done: state.done, messagesCount: state.messages.length, }); return state.done ? END : "agent"; }); - •
Set a lower recursion limit
- •Force failures earlier so you can inspect behavior faster.
- •In LangGraph JS/TS this usually surfaces as
GraphRecursionError.
- •
Diff input vs output state
- •Compare what each node receives and returns.
- •If
done,status, ormessagesnever changes, you found the loop source.
- •
Temporarily remove concurrency
- •Disable parallel branches and test one path at a time.
- •Scaling issues often hide a simple cycle that only becomes visible under load.
Prevention
- •
Always define a terminal condition for every cycle:
- •
ENDfor success - •explicit failure path for unrecoverable errors
- •
- •
Keep routing functions pure and deterministic:
- •route based on current state only
- •avoid reading mutable globals or shared caches
- •
Add regression tests for graph termination:
- •assert max step count
- •assert that known inputs reach
ENDwithin N transitions
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit