TL;DR
GraphRecursionError: Recursion limit of N reached without hitting a stop conditionmeans a custom LangGraph run executed N super-steps without any node routing toEND.Nis whateverrecursion_limitis set to for that run.- The familiar
25is the LangGraph JS default and was the LangGraph Python default before v1.0.6. Current LangGraph Python defaults much higher (the docs say 1000; the source constant is 10007), so if you are on recent Python and still see a small number, a template,langgraph-cli, or your own config set it. - The prebuilt
create_react_agentdoes not raise on this condition. When its step budget runs low it returns anAIMessagereadingSorry, need more steps to process this request.instead, so “the agent quietly gave up” andGraphRecursionErrorare two faces of the same loop. - Diagnose by streaming with
stream_mode="updates"(or a LangSmith trace) to see which node repeats, then fix the routing: make the conditional edge depend on state that actually advances, add a path toEND, or cap steps gracefully with theRemainingStepsmanaged value. - Legacy LangChain
AgentExecutorloops have their own signatures: a silentmax_iterationscutoff or anOutputParserExceptionretry storm (Could not parse LLM output:/... is not a valid tool, try another one.). Same idea, different knobs, covered at the end.
GraphRecursionError: what “recursion limit reached” actually means
When a LangGraph graph runs, execution advances in super-steps. A super-step is one round of the Pregel loop: every node with input available runs, and nodes that run in parallel in the same round count as a single super-step. recursion_limit is the ceiling on super-steps for a run. When the loop reaches it without any node handing control to END, LangGraph raises:
langgraph.errors.GraphRecursionError: Recursion limit of N reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.
N in that message is your configured limit, not a fixed constant. Where the well-known 25 comes from:
- LangGraph JS still defaults to
25. - LangGraph Python before v1.0.6 defaulted to
25as well. - LangGraph Python from v1.0.6 on raised the default sharply. The applied runtime value is the source constant
DEFAULT_RECURSION_LIMIT(currently10007, overridable with theLANGGRAPH_DEFAULT_RECURSION_LIMITenvironment variable); the docs round it down to “1000 steps.” That exact number is an internal detail and has shifted between releases, but it is firmly in the thousands, not 25.
So a small N on recent LangGraph Python is a signal in itself: a tutorial snippet, langgraph-cli, a project template, or an explicit {"recursion_limit": ...} in your own code set it. Search your codebase for recursion_limit before assuming the framework picked the number.
Whatever N is, the limit is a circuit breaker, not the bug. Hitting it tells you one of two things is true:
- the graph legitimately needs more than
Nsuper-steps for this input (plausible whenNis 25; almost never whenNis 1000+), or - the graph is cycling: a node, or a small group of nodes, keeps handing control back and forth and the state that should move the run toward a stop condition never changes.
Agents built with create_react_agent from langgraph.prebuilt are a two-node loop: the model node proposes tool calls, the tool node runs them, control returns to the model. If the model keeps asking for tools and never returns a final answer, the loop still has to be stopped, but the prebuilt agent stops it for you. Its default state carries a remaining_steps value, derived from recursion_limit minus the current step. After each model response, _are_more_steps_needed() checks whether that response still wants tools and how many steps are left; when only one or two remain, the agent returns AIMessage("Sorry, need more steps to process this request.") instead of proceeding. Because remaining_steps tracks recursion_limit, raising the limit never converts that message into a GraphRecursionError — you only get the exception from the prebuilt agent if you pass a custom state schema that omits remaining_steps. When recursion_limit is small (JS, older Python, or an explicit low setting) you hit this quickly and see the message; when it is in the thousands, the loop burns that many model calls first, so you usually notice the cost and latency before the message ever appears.
Sources: GRAPH_RECURSION_LIMIT, LangGraph Graph API
Step 1 — Unblock the run (and when raising the limit is legitimate)
First find out what your limit actually is and what set it. recursion_limit is a run-level config value, not a graph-construction argument, so grep for it in your own code, your templates, and any langgraph-cli config. If you are on recent LangGraph Python and the number in the error is small, that search is where the fix usually is — remove the low override, or set it deliberately.
Set it explicitly when you invoke or stream:
# per invocation
result = graph.invoke(inputs, {"recursion_limit": 50})
# streaming
for chunk in graph.stream(inputs, {"recursion_limit": 50}):
...
# bind it once to a prebuilt agent
agent = create_react_agent(model, tools).with_config({"recursion_limit": 50})
Sizing it: one tool call costs two super-steps (the model node, then the tool node). Count the tool calls a successful run makes, multiply by two for super-steps, then double again for retries and reflection. An agent that normally makes ten tool calls does ~20 super-steps of useful work, so recursion_limit around 40 leaves headroom without hiding a runaway; a fixed three-step pipeline runs fine with 10. A tight explicit limit like this is worth keeping on LangGraph Python too — the ~1000/10007 default is so high that a real loop wastes thousands of model calls before it trips.
Do not reach for a huge number to make the error disappear. If you cannot explain why the run needs that many super-steps, a bigger limit only buys a longer, more expensive failure. Diagnose first.
Sources: GRAPH_RECURSION_LIMIT
Step 2 — Find the node that is looping
The fastest way to see a loop is to print what each super-step does. Stream with stream_mode="updates", which yields one {node_name: state_delta} dict per node execution:
for step in graph.stream(inputs, stream_mode="updates"):
for node, delta in step.items():
print(node, "->", delta)
In a healthy run the node names advance and each delta carries new information. In a loop you see the same node (or the same short A -> B -> A sequence) repeating, and the deltas are empty, identical, or oscillating between two values. That node, and the edge that routes back into it, is where the bug is.
A few more ways to narrow it down:
stream_mode="debug"emitstaskandtask_resultevents with the full input and output of every node, which is useful when the delta alone does not explain the routing decision.- A LangSmith trace renders the run as a tree; a cycle shows up as an obviously repeating branch. This is the least effort option if tracing is already enabled.
- Inside any node you can read
config["metadata"]["langgraph_step"]to know which super-step you are on, which is handy for a targetedprintor breakpoint once you know roughly where the loop is.
Sources: LangGraph Graph API
Step 3 — Fix the actual cause
Once you know which node repeats, the cause is almost always one of these.
The router never advances
A conditional edge decides the next node from a field in state. If the node that is supposed to update that field does not (a missing return key, an overwritten reducer, a typo), the router sees the same value forever and keeps choosing the same branch.
def route(state: State) -> str:
# loops forever if `state["status"]` is never set to "done" by any node
return END if state["status"] == "done" else "worker"
Fix it by making sure the node writes the field the router reads, and by routing on something that measurably progresses (a counter, a shrinking work queue, a done flag that a node actually sets).
There is no path to END
Every cyclic graph needs at least one edge, usually a conditional one, that can reach END. If every branch leads back into the cycle, the only exit is the recursion limit. Add the terminating condition explicitly.
create_react_agent keeps asking for tools
The symptom is usually the Sorry, need more steps to process this request. message (a higher recursion_limit only makes the run longer and more expensive before it returns that same message), because the model keeps emitting tool calls instead of a final answer. Common reasons: a tool always raises and returns an unhelpful error string, so the model retries it; the tool docstring does not say what the result means or when the task is complete; the system prompt never tells the model to answer directly once it has enough information. Fix the tool’s description and its error/return payload, add an explicit “when you have the answer, respond without calling a tool” instruction, raise recursion_limit only if the task genuinely needs more calls, and deduplicate repeated calls (below).
Two nodes ping-pong
Unconditional A -> B and B -> A edges are an infinite loop by construction. One of the two edges has to be conditional and able to leave the cycle.
Cap steps gracefully with RemainingSteps
To return a partial answer instead of crashing, add the RemainingSteps managed value to your state. LangGraph populates it with how many super-steps are left before the limit, so a node can bail out early:
from langgraph.managed import RemainingSteps
from typing import Annotated, TypedDict
from operator import add
class State(TypedDict):
messages: Annotated[list, add]
remaining_steps: RemainingSteps
def worker(state: State):
if state["remaining_steps"] <= 2:
return {"messages": [("assistant", "Stopping early with a partial result.")]}
...
Break out from inside a node with Command
A node (or a tool in LangGraph) can return a Command to both update state and jump straight to a terminal node, which is useful when a node detects a cancellation or an unrecoverable condition mid-run:
from langgraph.graph import END
from langgraph.types import Command
def guard(state: State) -> Command:
if state.get("cancelled"):
return Command(goto=END, update={"messages": [("assistant", "Cancelled.")]})
return Command(goto="worker")
Sources: LangGraph Graph API, ReAct Agent doesn't throw GraphRecursionError
Loop-detection guard: stop repeated tool calls
Most runaway agents repeat the same action. A small amount of state plus one check catches that before the recursion limit does. Record a signature of each tool call and stop when it repeats consecutively:
import json
def tool_guard(state: State):
calls = state.get("recent_calls", [])
last = state["messages"][-1]
sig = None
if getattr(last, "tool_calls", None):
tc = last.tool_calls[0]
sig = json.dumps([tc["name"], tc["args"]], sort_keys=True)
if sig and calls[-2:] == [sig, sig]: # 3rd identical call in a row
return {"messages": [("assistant",
"Repeated the same tool call three times; stopping to summarize.")],
"route": "summarize"}
return {"recent_calls": (calls + [sig])[-5:] if sig else calls}
Wire it in as a node between the model and the tools, or as pre_model_hook / middleware if you are on a framework version that supports it. The point is the same: detect “no progress” (identical payloads, repeated stderr, the same observation N times) and force a stop, summarize, propose alternatives turn instead of another identical step.
Sources: LangGraph Graph API
Legacy LangChain AgentExecutor loops
The pre-LangGraph agent runtime, AgentExecutor (now shipped in langchain / langchain-classic), has its own loop controls and its own failure signatures.
Execution caps
max_iterationslimits intermediate steps and defaults to 15. Setting it toNoneremoves the cap entirely, which is what turns a misbehaving agent into an unbounded one.max_execution_time(defaultNone) is a wall-clock limit in seconds.early_stopping_method(default"force") controls what happens when a cap is hit."force"returns a fixed response,Agent stopped due to iteration limit or time limit.The API also documents"generate"(one more LLM call to synthesize an answer from the steps gathered so far), but several LangChain versions raise aValueErrorabout an unsupportedearly_stopping_methodwhen you actually pass it, so treat"force"as the value you can rely on.
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
max_iterations=15,
max_execution_time=60,
early_stopping_method="force",
handle_parsing_errors=True,
)
ReAct parsing deadlocks
A ReAct agent ends a run by emitting an exact string (historically a Final Answer: line). If the model’s format drifts, the output parser raises OutputParserException: Could not parse LLM output:. With handle_parsing_errors=True the executor feeds the error back and asks the model to reformat; a model that never produces a parseable answer will do this until max_iterations cuts it off. Tightening the format instructions in the prompt is the real fix; handle_parsing_errors is a seatbelt.
The “not a valid tool” loop
A recurring report: an agent calls a tool name that is not registered, gets back ... is not a valid tool, try another one., and its next thought decides it still needs that same tool. It calls the invalid tool again, gets the same observation, and repeats until it gives up with “I don’t know”. One documented case hit this while inspecting a SQL database (repeated list_tables_sql_db calls on LangChain 0.0.215 / Python 3.10.11); a related case appeared when an agent was wrapped as a tool and loaded into a second agent, after which every tool call in the outer agent came back invalid. The mechanism is the same as the modern create_react_agent case: a bad observation the model is not able to recover from, repeated because nothing stops it.
New code should prefer LangGraph or create_react_agent over AgentExecutor, but the diagnosis is identical: find the step that repeats, then remove the reason it repeats.
Sources: AgentExecutor reference, list_tables_sql_db is not a valid tool, try another one., {tool_name} is not a valid tool, try another one.
Closing thoughts
recursion_limit and max_iterations are financial circuit breakers. They keep a broken run from getting expensive, but a run that hits them is telling you the graph has no reliable path to a stop condition for that input.
The durable fix is a graph whose state moves monotonically toward termination: every cycle has a conditional edge that can reach END, the field that edge checks is written by a node on every pass, and “no progress” is itself a terminal condition rather than something you wait out. Instrument the run with stream_mode="updates" or LangSmith so a loop is visible in seconds, add RemainingSteps so the graph degrades to a partial answer instead of an exception, and add a small duplicate-call guard so the common “same action forever” failure is caught early. Keep an explicit, tight recursion_limit regardless of platform, since the current LangGraph Python default is high enough to let a loop run for thousands of steps. With those in place, an agent that wanders into a bad path pauses and pivots instead of repeating until the limit and crashing.
Frequently Asked Questions
How do I fix GraphRecursionError in LangGraph?
Decide first whether the graph is looping or just long. Stream the run with stream_mode="updates" and look for a node that repeats with no change to state. If it is a real loop, fix the routing: make the conditional edge depend on a state field that a node updates every pass, ensure some branch can reach END, or add a RemainingSteps check that returns a partial result. Only if the graph genuinely needs more steps, raise the cap for that call with graph.invoke(inputs, {"recursion_limit": 50}).
What does “Recursion limit of N reached without hitting a stop condition” mean?
Your LangGraph run executed N super-steps (rounds of the node-execution loop) and no node ever routed to END. N is whatever recursion_limit was set to for the run. The message almost always indicates a cycle where the state that should trigger termination never changes.
What is the default recursion limit in LangGraph?
It depends on the platform and version. LangGraph JS defaults to 25. LangGraph Python defaulted to 25 through v1.0.5, then raised it from v1.0.6 on — the applied value is the source constant DEFAULT_RECURSION_LIMIT (currently 10007, settable via LANGGRAPH_DEFAULT_RECURSION_LIMIT), which the docs simplify to “1000 steps.” Treat it as “in the thousands”; the exact figure is an internal detail. Parallel nodes in one round count as a single super-step, so the limit is on rounds of the loop, not total node executions.
I am on recent LangGraph Python and still see “Recursion limit of 25” — why?
Because 25 is no longer the Python default. Something set it: a tutorial or docs snippet, a langgraph-cli config, a project template, create_react_agent example code, or an explicit {"recursion_limit": 25} in your invoke/stream call. Grep your code and config for recursion_limit.
Should I just increase recursion_limit?
Only if you can explain why the run needs more super-steps. Raising it is correct for a legitimately long graph on a low limit. If you do not know why the graph loops, a higher limit just produces a slower, more expensive failure; diagnose the cycle first.
How do I find which node is causing the loop?
Run for step in graph.stream(inputs, stream_mode="updates"): print(step) and watch the node names. A loop shows up as the same node, or a short repeating sequence of nodes, emitting empty or identical state deltas. stream_mode="debug" and a LangSmith trace give the same picture with more detail.
How is recursion_limit different from LangChain’s max_iterations?
recursion_limit is a LangGraph run-config value that caps super-steps and raises GraphRecursionError when a custom graph exceeds it. max_iterations is an AgentExecutor constructor argument that caps intermediate steps in the legacy agent runtime and, on reaching the cap, returns a stopped-response string rather than raising (default 15).
How do I return a partial result instead of raising GraphRecursionError?
Add the RemainingSteps managed value to your state and check it inside your nodes: when remaining_steps is down to 1 or 2, return a summary or route to a dedicated “wrap up” node instead of continuing the loop. That way the graph stops itself before LangGraph’s hard limit does.
Why does my LangGraph agent return “Sorry, need more steps to process this request.” instead of an error?
That message comes from the prebuilt create_react_agent. Its state includes a remaining_steps budget, and when it is nearly exhausted the agent returns AIMessage("Sorry, need more steps to process this request.") rather than raising GraphRecursionError. It means the same thing as the exception: the model kept calling tools without producing a final answer. Raise recursion_limit only if the task genuinely needs more calls; otherwise diagnose the tool/model loop with stream_mode="updates".
