Tutorials
Ren Okabe5 min read1 views

LangGraph Recursion Limit: Fix GraphRecursionError Properly (2026)

GraphRecursionError means you hit LangGraph's 25-superstep recursion limit. Raise it the right way, find the real infinite loop, and exit gracefully with RemainingSteps.

Macro photograph of a circuit board, evoking the cyclic execution paths that trigger a LangGraph recursion limit.
Macro photograph of a circuit board, evoking the cyclic execution paths that trigger a LangGraph recursion limit.
On this page

Quick Answer (2026)

LangGraph raises GraphRecursionError when your graph runs more supersteps than its recursion_limit, which defaults to 25. You can raise the ceiling by passing {"recursion_limit": 100} in the config at invoke time, but that only helps when your agent genuinely needs more steps. Most of the time the error means a cyclic graph is looping forever because a conditional edge never routes to END. This tutorial reproduces the error in 20 lines, shows the correct one-line raise, then shows the real fix: terminate the cycle, bound the loop, and exit gracefully with RemainingSteps.

LangChain If you build agents with Python Python and LangGraph, you will hit Recursion limit of 25 reached without hitting a stop condition sooner or later. Here is exactly what it means and how to fix it for good.

What the error actually means

Every LangGraph run executes in supersteps. One superstep is one round of the graph advancing, following the Pregel execution model. After each superstep, LangGraph checks a counter against recursion_limit. When the counter passes the limit, it stops and raises:

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

Two facts to anchor on:

  1. The default recursion_limit is 25, and it is always present in the run config.
  2. The limit counts supersteps, not nodes. Nodes that run in parallel in the same round count as one step. A linear chain of 30 nodes is 30 steps; a fan-out of 30 parallel nodes is closer to 1 step.

The limit is a safety valve. It exists so a buggy cyclic graph fails loudly instead of running (and billing) forever.

Reproduce it in 20 lines

A graph with a cycle and no exit condition will always trip the limit. Run this and watch it fail at 25:

python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    count: int

def increment(state: State) -> State:
    return {"count": state["count"] + 1}

builder = StateGraph(State)
builder.add_node("increment", increment)
builder.add_edge(START, "increment")
builder.add_edge("increment", "increment")  # cycle with no way out

graph = builder.compile()
graph.invoke({"count": 0})
# langgraph.errors.GraphRecursionError: Recursion limit of 25 reached ...

Nothing here ever routes to END, so the graph loops until the safety valve fires.

The shallow fix: raise the limit (and when it is correct)

You can raise recursion_limit at invoke time. All three of these work:

python
# positional config
graph.invoke({"count": 0}, {"recursion_limit": 100})

# explicit config kwarg (same thing, clearer in larger codebases)
graph.invoke({"count": 0}, config={"recursion_limit": 100})

# bake it into a reusable runnable
graph.with_config(recursion_limit=100).invoke({"count": 0})

If you run on LangGraph Platform or through langgraph-cli, pass the same key in the run config of the request body: {"config": {"recursion_limit": 100}}.

Here is the trap: on the infinite graph above, raising the limit to 100 just moves the crash from step 25 to step 100. Raising the ceiling is the right call only when the work is genuinely long, for example a deep ReAct tool loop or a multi-agent handoff that legitimately needs 40 turns. If you are not sure the loop terminates, do not raise the limit. Fix the loop.

The real fix: give the cycle an exit

Nine times out of ten, GraphRecursionError is an infinite loop wearing a config problem as a costume. The durable fix is a conditional edge that can reach END:

python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    count: int

def increment(state: State) -> State:
    return {"count": state["count"] + 1}

def should_continue(state: State) -> str:
    if state["count"] >= 5:
        return "stop"
    return "loop"

builder = StateGraph(State)
builder.add_node("increment", increment)
builder.add_edge(START, "increment")
builder.add_conditional_edges(
    "increment",
    should_continue,
    {"loop": "increment", "stop": END},
)

graph = builder.compile()
print(graph.invoke({"count": 0}))  # {'count': 5}

The most common bug in real agents is a router that never returns its stop branch, or a mapping key that does not match the string the router returns. If your router returns "done" but your mapping only has {"continue": ..., "end": END}, the run keeps looping. Print the router's return value once and confirm it matches a key that points at END.

Bound agent loops with a step budget in state

For a tool-calling agent that decides on its own when to stop, add an explicit budget to state so a stuck model cannot loop past a number you control:

python
def route(state: State) -> str:
    if state["count"] >= state.get("max_steps", 8):
        return "stop"
    return "loop"

This keeps the hard recursion_limit as a last-resort safety valve while your own budget does the real governing. If you are running longer stateful agents, pair this with a checkpointer so a bounded run can pause and resume. See our companion tutorial on the langgraph-checkpointer for that setup.

Exit gracefully before the hard error with RemainingSteps

Sometimes you want a partial answer instead of an exception. LangGraph exposes a managed value, RemainingSteps, that it injects and decrements every superstep. Read it in your router and bail out with headroom to spare:

python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.managed.is_last_step import RemainingSteps

class State(TypedDict):
    count: int
    remaining_steps: RemainingSteps

def increment(state: State) -> State:
    return {"count": state["count"] + 1}

def route(state: State) -> str:
    # leave 2 steps of headroom so we return a result, not a crash
    if state["remaining_steps"] <= 2:
        return "stop"
    return "loop"

builder = StateGraph(State)
builder.add_node("increment", increment)
builder.add_edge(START, "increment")
builder.add_conditional_edges("increment", route, {"loop": "increment", "stop": END})

graph = builder.compile()
print(graph.invoke({"count": 0}, {"recursion_limit": 10}))

You never set remaining_steps yourself; LangGraph manages it. This is exactly how create_react_agent avoids crashing when a model will not stop calling tools. It returns the best answer it has instead of raising.

Always catch the exception at the boundary

Even with a budget and a graceful exit, keep the hard error caught at your top-level call so a runaway run degrades instead of taking down the request:

python
from langgraph.errors import GraphRecursionError

try:
    result = graph.invoke(inputs, {"recursion_limit": 50})
except GraphRecursionError:
    # log the run id, return a cached or partial answer, or escalate
    result = {"status": "incomplete", "reason": "recursion_limit"}

Debugging checklist

  • Print your conditional router's return value and confirm it exactly matches a mapping key that points at END.
  • Confirm at least one path in every cycle can reach END. A cycle with no terminal branch always trips the limit.
  • Ask whether the run genuinely needs more than 25 supersteps before raising the limit. If it does not, the loop is the bug.
  • Remember parallel nodes count as one superstep, so wide fan-outs rarely need a higher limit.
  • Add a max_steps budget in state for any agent that decides its own stopping point.
  • Use RemainingSteps when a partial answer beats an exception, and still wrap the top-level call in try/except GraphRecursionError.

For the exact counter semantics and platform config keys, the LangGraph error reference (2026) is the authoritative source, and the LangGraph source on GitHub shows how the superstep counter and managed values are implemented. If your loop is really an eval-and-retry cycle, the same termination discipline applies; see our walkthrough of the agentic-workflow-evaluator-optimizer-python pattern.

Sources

R

Written by

Ren Okabe

Ren Okabe builds and debugs LLM agent systems, with a focus on LangGraph orchestration, evals, and production reliability.

Frequently asked questions

What is the default recursion limit in LangGraph?

The default recursion_limit is 25 supersteps. When a run exceeds it, LangGraph raises GraphRecursionError. The limit is always present in the run config.

How do I increase the LangGraph recursion limit?

Pass a higher value in the config at invoke time, for example graph.invoke(inputs, {"recursion_limit": 100}). You can also use graph.with_config(recursion_limit=100), or set the same key in the run config on LangGraph Platform and langgraph-cli.

Does raising recursion_limit fix GraphRecursionError?

Only when the work is genuinely long. Most of the time the error is an infinite loop, so raising the limit just moves the crash from step 25 to your new number. Fix the loop first.

What counts as one recursion step in LangGraph?

One superstep, following the Pregel model, not one node. Nodes that run in parallel in the same round count as a single step, so wide fan-outs rarely need a higher limit.

How do I stop a LangGraph agent before it hits the limit?

Add a max_steps budget to state and check it in a conditional edge, or read the managed RemainingSteps value and route to END with a couple of steps of headroom so you return a partial answer instead of an exception.

Why does my conditional edge still loop forever?

Usually the router never returns its stop branch, or the string it returns does not match a mapping key that points at END. Print the router's return value once and confirm it maps to END.