Guide
LangGraph agents fail differently from scripts. A script stops at the line that broke. A graph keeps routing, retrying and looping, so by the time you see an error, the cause is several steps back. To debug LangGraph agents in production, build three habits: make the graph's state inspectable, make its loops bounded, and make failures land somewhere you can see them.
When a graph runs out of steps, LangGraph raises GraphRecursionError with a message like:
Recursion limit of N reached without hitting a stop condition.
You can increase the limit by setting the `recursion_limit` config key.
The message suggests raising the limit. Usually that is the wrong first move. The error means a path through the graph never reached END, and the common cause is a conditional edge that keeps sending the agent back to the same node, for example a tool call that fails and is retried by the model forever.
Check the routing first. When the loop is genuinely long and intended, raise the limit per run:
graph.invoke(inputs, {"recursion_limit": 100})
or set it once on the compiled graph:
graph = builder.compile().with_config(recursion_limit=100)
If you use the prebuilt ReAct agent, it tracks remaining_steps and returns a final message ("Sorry, need more steps to process this request.") instead of raising, once it is nearly out of steps. That is better for users, and worse for debugging, because the failure no longer looks like an error. Log it.
Most production debugging questions are "what did the agent know at step N?". A checkpointer answers that. Compile with one and give every run a thread_id:
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "ticket-4812"}}
graph.invoke(inputs, config)
state = graph.get_state(config) # current values and the next node
history = list(graph.get_state_history(config)) # every checkpoint, newest first
Use a durable checkpointer in production, not the in-memory one, or the history disappears with the process. Use a thread_id you can find later, such as a ticket or request ID, so a bug report leads straight to its run.
Once you have the history, you can rerun from any checkpoint by passing its config to invoke:
before = next(s for s in history if s.next == ("call_tool",))
graph.invoke(None, before.config)
Nodes before that checkpoint are not re-executed. Nodes after it are, including LLM calls and API requests, so replaying against production tools has real side effects. Point replays at a staging environment, or at tools that are safe to call twice.
To test a fix, update_state on a past checkpoint creates a fork with changed values, and invoking from the fork shows whether the graph now takes the path you expect. Subgraphs replay as a single step unless they were compiled with their own checkpointer (checkpointer=True).
For live debugging, stream what each node returns instead of waiting for the final result:
for chunk in graph.stream(inputs, stream_mode="updates", version="v2"):
if chunk["type"] == "updates":
for node, update in chunk["data"].items():
print(node, update)
A loop shows up quickly as the same node name repeating with nearly identical updates.
A single flaky tool should not take down a run. In recent LangGraph versions you can set retry, timeout and error handling for every node at once:
graph = (
StateGraph(State)
.set_node_defaults(
retry_policy=RetryPolicy(max_attempts=3),
timeout=TimeoutPolicy(run_timeout=30),
error_handler=record_failure,
)
...
)
The error handler runs after retries are exhausted. It can write the failure into state, or return a Command that routes to a recovery node. Values set on an individual node override the defaults. Either way, the failure becomes a state change you can inspect later instead of an exception that vanished with the process.
thread_id and a durable checkpointer.If you are running CrewAI as well, the same thinking applies there. See debugging CrewAI agents in production. For what to capture across a fleet of agents, see AI agent observability.
FleetHelp's managed support is for teams running LangGraph, CrewAI or custom agents in production who want an experienced second pair of eyes on stuck runs, loops and failures, without hiring for it.
First find out why the graph never reached a stop condition, usually a conditional edge that keeps routing back to the same node. Only raise the limit once the loop is intentional, by passing recursion_limit in the run config or baking it in with with_config.
Compile the graph with a checkpointer and give each run a thread_id. get_state shows the current state and the next node, and get_state_history lists every checkpoint, newest first.
Yes. Pass a past checkpoint's config to invoke. Nodes before that checkpoint are not re-run, and nodes after it run again, including their LLM and API calls.
Give the node a retry policy and an error handler. In recent LangGraph versions you can set these for every node with set_node_defaults, and the handler can record the failure or route the graph to a recovery step.