Back to Intelligence Hub
DebuggingSeptember 26, 2026

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."

Observyze Engineering
Observyze Engineering
Runtime Systems & AI Reliability
11 min read

The Observyze Engineering team builds production-grade telemetry, circuit breakers, and runtime governance tools for distributed multi-agent systems and LLM pipelines.

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

Python Traceback — LangGraph 0.2.x
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.

Broken — task_complete never set
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 loop
Fixed — check tool_calls on last message
from 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:

Python — iteration_guard.py
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:

Python — non_retryable_tool_error.py
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)
Technical FAQ

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.

Ready to Govern your Inference?

Request Early Access to evaluate Observyze against a representative AI workflow.