How to Debug "Maximum Recursion Depth Exceeded" in LangGraph
"Root cause analysis and fixes for GraphRecursionError in LangGraph StateGraph workflows — covering conditional edge loops, missing termination logic, and runtime recursion_limit configuration."
Direct Answer
LangGraph's GraphRecursionError: Recursion limit of X reached occurs when should_continue never returns END — typically because the termination check references a state key that no node updates. Fix by checking AIMessage.tool_calls absence and adding a hard iteration counter to state.
The Exact Error
langgraph.errors.GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition. You can increase the limit by setting the "recursion_limit" config key.
Each node invocation costs 1 step. An agent→tool_node→agent cycle = 2 steps per turn, giving you ~12 turns before the default limit of 25 is reached.
Root Cause 1 — should_continue with Unreachable END
The most common cause: the termination condition checks a flag that no node ever sets to True.
from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal
class AgentState(TypedDict):
messages: list[dict]
task_complete: bool # never set to True by any node
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
if state["task_complete"]: # ← always False
return END
return "tools" # ← always taken → infinite loopfrom langchain_core.messages import AIMessage
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
last = state["messages"][-1]
# If LLM produced no tool calls, it has finished reasoning
if isinstance(last, AIMessage) and not last.tool_calls:
return END
return "tools"Root Cause 2 — No Hard Iteration Ceiling in State
Even with a correct termination check, adversarial inputs or LLM reasoning failures can keep the agent looping. Add an explicit iteration counter as a hard ceiling:
from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal, Annotated
from langchain_core.messages import BaseMessage, AIMessage
import operator
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], operator.add]
iteration: int
MAX_ITERATIONS = 15
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
if state["iteration"] >= MAX_ITERATIONS:
return END # always terminates — hard ceiling
last = state["messages"][-1]
if isinstance(last, AIMessage) and not last.tool_calls:
return END
return "tools"
def agent_node(state: AgentState) -> dict:
response = llm_with_tools.invoke(state["messages"])
return {
"messages": [response],
"iteration": state["iteration"] + 1, # increment each turn
}
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.set_entry_point("agent")
builder.add_conditional_edges("agent", should_continue)
builder.add_edge("tools", "agent")
graph = builder.compile()
result = graph.invoke(
{"messages": [HumanMessage(content=query)], "iteration": 0},
config={"recursion_limit": 50}, # safety net — state counter trips first
)Root Cause 3 — Tool Node That Fails Deterministically
If your tool raises an exception for a non-retryable failure (permission denied, invalid schema), the LLM will attempt to reformulate and retry infinitely. Return a structured error string instead of raising:
from langchain_core.tools import tool
@tool
def database_query(sql: str) -> str:
"""Execute a read-only SQL query."""
try:
return db.execute(sql).to_json()
except PermissionError:
# Return FATAL: prefix — do NOT raise
# Raising feeds the error back to the agent,
# which reformulates and retries indefinitely
return "FATAL: Insufficient permissions. Do not retry this tool."
except Exception as e:
return f"ERROR ({type(e).__name__}): {e}. Retry may help."Configure should_continue to return END if the last tool message starts with FATAL:.
LangGraph Recursion Debug Checklist
- ✓Verify should_continue returns END when last message is AIMessage with no tool_calls
- ✓Add an iteration counter to AgentState and check it before other termination conditions
- ✓Set recursion_limit in graph.invoke() config as a hard safety net
- ✓In tool nodes, return FATAL: prefixed strings for non-retryable errors — do not raise
- ✓Enable trace recording to identify which node repeats and what state values are stale
- ✓Verify that tool return values are actually being added to state (check operator.add on messages)
Frequently Asked Questions
Q1.What causes GraphRecursionError in LangGraph?
GraphRecursionError is thrown when LangGraph's internal step counter reaches the recursion_limit set during compilation (default: 25). It occurs when should_continue never returns END — commonly because termination logic references state keys that are never updated by any node, causing the agent to loop with the same routing decision.
Q2.What is the default recursion_limit in LangGraph?
As of LangGraph 0.2.x, the default recursion_limit is 25 steps. Each node invocation counts as one step. An agent→tools→agent cycle = 2 steps per turn, meaning 25 steps supports roughly 12 agent turns before the error is raised.
Q3.How do I increase the recursion limit in LangGraph?
Pass recursion_limit in the config to graph.invoke(): graph.invoke(state, config={"recursion_limit": 50}). However, increasing the limit is not a fix — it delays the error. The root cause is always a missing or unreachable termination branch in should_continue.
Q4.How do I detect which node is causing the infinite loop?
Enable distributed tracing (Observyze or LangSmith) and inspect the step sequence. A loop appears as a repeating pattern of node invocations with near-identical state values. If the tool return value is not mutating agent state, the agent has no new information to reason from and will repeat the same tool call indefinitely.
Related Technical Articles
Metadata-Only Telemetry: Reducing Sensitive Data in LLM Observability
How direct provider routing, content omission, and deterministic local redaction reduce telemetry exposure—and where their limits remain.
The Rise of Autonomous Agentic Governance
Why the next generation of AI requires a fundamental rethink of infrastructure and safety protocols.
Why Traditional Monitoring Is Not Enough for LLM Applications
Separating post-hoc observability, pre-dispatch policy checks, and asynchronous output evaluation.
Ready to Govern your Inference?
Request Early Access to evaluate Observyze against a representative AI workflow.