Menu

LangGraph Cycles and Recursion Limits — How to Control Agent Loops

Written by Selva Prabhakaran | 21 min read

Your ReAct agent handles simple questions just fine. It calls a tool, reads the result, and replies. But then someone asks something vague. The LLM fires the same tool over and over. Twenty-five rounds later, your terminal blows up with GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition. If that sounds familiar, this guide is for you. You’ll learn why it happens, how to tame it, and how to build agents that quit on their own instead of crashing.

What Are Cycles in a LangGraph Graph?

A cycle is a path that sends the graph back to a node it already ran. Most graph tools ban this — they only allow DAGs (directed acyclic graphs) where data flows one way and each node runs once. LangGraph breaks that rule on purpose because agent loops need it.

Before You Start

  • Python: 3.10+
  • Package: langgraph 0.4+
  • Install: pip install langgraph
  • Background: LangGraph basics — nodes, edges, state, conditional edges
  • Time: 20–25 minutes

Here’s the simplest cycle you can build. The increment node adds 1 to a counter. A conditional edge checks — if the count is under 3, go back to increment. If not, go to END.

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

class CounterState(TypedDict):
    count: int
    message: str

def increment(state: CounterState) -> dict:
    new_count = state["count"] + 1
    return {"count": new_count, "message": f"Step {new_count}"}

def should_continue(state: CounterState) -> str:
    if state["count"] < 3:
        return "increment"
    return END

graph = StateGraph(CounterState)
graph.add_node("increment", increment)
graph.add_edge(START, "increment")
graph.add_conditional_edges("increment", should_continue)

app = graph.compile()
result = app.invoke({"count": 0, "message": ""})
print(result)
python
{'count': 3, 'message': 'Step 3'}

The node ran three times. After each run, the router checked the counter and looped back — until the count hit 3 and the graph stopped. That’s a cycle: a node that visits itself again through a conditional edge.

Key Insight: Cycles are what make LangGraph agents tick. Without them, every node runs once and the graph ends. With them, an agent can think, act, check the result, and decide to keep going — the core of every ReAct loop.

Why Do Agents Need Cycles?

If cycles are risky, why use them at all? Because three key agent patterns fall apart without them.

The ReAct loop. The agent picks a tool, runs it, reads what came back, and then picks another tool or writes a final answer. That decide-act-observe cycle keeps going until the agent has enough info.

Draft-and-refine loops. One node writes a draft. Another node scores it. If the score is too low, the graph sends it back for another try. The loop runs until the output clears a quality bar.

Page-by-page data fetching. An agent hits an API, sees there are more pages, and loops back to grab the next one. It stops when there’s nothing left to fetch.

All three need a cycle. And all three can spin forever if you skip the exit check.

Warning: Every cycle needs a way out. If your conditional edge can send the graph back to an earlier node, it must also be able to send it to END. Without that, the graph loops until it hits the limit and crashes.

When Should You Skip Cycles?

Not every job calls for a loop. If your pipeline runs a set series of steps — like extract, clean, load — a straight-line graph with plain edges is the right call. No cycle needed.

If you’re handling a batch of items that don’t depend on each other, use a map-reduce pattern instead. Fan out to parallel nodes, then combine the results. It’s faster and kills the risk of runaway loops.

A handy rule: if you know the step count when you build the graph, skip the cycle. Cycles pay off when the number of rounds depends on what happens at runtime — LLM choices, quality scores, or API pagination.

How Does recursion_limit Work?

LangGraph counts each node run as one step. The recursion_limit setting caps the total steps a graph can take before it throws GraphRecursionError. The default cap is 25 steps.

You pass it through the config dict when you call invoke() or stream(). Here are three ways to set it.

python
# Default: 25 steps
result = app.invoke({"count": 0, "message": ""})

# Custom: allow up to 50 steps
result = app.invoke(
    {"count": 0, "message": ""},
    {"recursion_limit": 50}
)

# Tight limit: only 5 steps allowed
result = app.invoke(
    {"count": 0, "message": ""},
    {"recursion_limit": 5}
)

Think of recursion_limit as a circuit breaker. It doesn’t steer your graph’s logic — it’s a safety net that kills the run if your logic fails to stop the loop on its own.

Quick check: Say your graph has a 4-node cycle and you set the limit to 25. How many full passes can it make? About 6 — because 25 / 4 = 6.25, and it truncates to 6 full passes.

The same cap works with stream(). Each event the stream yields still counts as a step.

python
# Streaming also respects recursion_limit
for event in app.stream(
    {"count": 0, "message": ""},
    {"recursion_limit": 30}
):
    print(event)

Tip: Set recursion_limit by hand for every graph you ship. Don’t lean on the default of 25. If your agent usually needs 10 tool calls, pick 30–40. If it’s a short 3-step pipeline, pick 10. Spelling it out tells the next dev what “normal” looks like for this graph.

What Is GraphRecursionError?

When the step count goes past the limit, LangGraph throws GraphRecursionError. It’s a plain Python exception — you catch it with try/except like any other.

Let’s trigger one on purpose. This graph has no way out — the router always sends it back to loop_node.

python
from langgraph.errors import GraphRecursionError

class LoopState(TypedDict):
    value: int

def loop_node(state: LoopState) -> dict:
    return {"value": state["value"] + 1}

def should_loop(state: LoopState) -> str:
    return "loop_node"  # always loops — no exit!

bad_graph = StateGraph(LoopState)
bad_graph.add_node("loop_node", loop_node)
bad_graph.add_edge(START, "loop_node")
bad_graph.add_conditional_edges("loop_node", should_loop)

bad_app = bad_graph.compile()

try:
    bad_app.invoke({"value": 0}, {"recursion_limit": 5})
except GraphRecursionError as e:
    print(f"Caught it: {e}")
python
Caught it: Recursion limit of 5 reached without hitting a stop condition. If you are using a graph with a cycle, set a higher recursion limit.

The error message tells you what limit you hit and hints that you should raise it. But if your graph truly has no exit, bumping the limit just delays the crash. The real fix is to add logic that routes to END at some point.

How Do You Debug Runaway Loops? A Three-Step Method

Your graph just threw GraphRecursionError and you didn’t expect it. Here’s a quick checklist that catches the bug 90% of the time.

Step 1 — Print the routing choices. Drop a print statement inside your router. You’ll see right away whether it ever picks END.

python
def should_continue_debug(state: CounterState) -> str:
    decision = "increment" if state["count"] < 3 else END
    print(f"  count={state['count']}, routing to: {decision}")
    return decision

Step 2 — Check that the node updates the right field. The top cause of endless loops isn’t bad routing logic — it’s a node that never changes the value the router looks at. If your router reads state["count"] but the node forgets to bump count, the check stays true forever.

Try to guess the outcome: what happens when you run the graph below? The node returns {"message": "done"} but never touches count.

python
def broken_node(state: CounterState) -> dict:
    return {"message": "done"}  # oops — count never changes

def check_count(state: CounterState) -> str:
    return END if state["count"] >= 3 else "broken_node"

Answer: endless loop. The router checks count, but count sits at 0 forever because the node never moves it.

Step 3 — Watch for off-by-one mistakes. If the node adds 1 and the router checks < 3, the cycle runs 3 times (count goes 0 → 1 → 2 → 3, exits when count hits 3). Make sure the boundary matches what you want.

Key Insight: Most runaway loops come from state bugs, not routing bugs. The router usually has the right condition. The problem is that the node it loops back to never changes the field the condition checks.

Exit Strategy 1 — How Does a State Counter Work?

The simplest way to stop a loop is a counter in your state. Each pass bumps the counter. When it reaches a cap, the router sends the graph to END.

Here’s the pattern for a real agent loop. The agent_step node tracks rounds in a counter field and writes a fallback message when it hits the cap. The route_agent function checks a done flag and either loops or exits.

python
class AgentState(TypedDict):
    query: str
    iterations: int
    max_iterations: int
    result: str
    done: bool

def agent_step(state: AgentState) -> dict:
    iteration = state["iterations"] + 1
    if iteration >= state["max_iterations"]:
        return {
            "iterations": iteration,
            "result": "Reached max iterations. Best answer so far.",
            "done": True,
        }
    return {
        "iterations": iteration,
        "result": f"Working... (iteration {iteration})",
        "done": False,
    }

def route_agent(state: AgentState) -> str:
    if state["done"]:
        return END
    return "agent_step"

Build and run it with a 3-round cap.

python
agent_graph = StateGraph(AgentState)
agent_graph.add_node("agent_step", agent_step)
agent_graph.add_edge(START, "agent_step")
agent_graph.add_conditional_edges("agent_step", route_agent)

agent_app = agent_graph.compile()

result = agent_app.invoke({
    "query": "What is the capital of France?",
    "iterations": 0,
    "max_iterations": 3,
    "result": "",
    "done": False,
})
print(f"Iterations: {result['iterations']}")
print(f"Result: {result['result']}")
python
Iterations: 3
Result: Reached max iterations. Best answer so far.

No crash, no error. The agent stopped after 3 rounds. And since max_iterations lives in the state, you can tune it per request — tight caps for easy questions, bigger caps for hard research tasks.

Exit Strategy 2 — How Does a Quality Gate Work?

What if you can’t predict how many rounds the agent needs? Sometimes the right answer isn’t “stop after N loops” but “stop when the result is good enough.”

Picture this: a graph writes a draft and scores it. If the score clears 0.8, it’s done. If not, it loops back for another try. But what if the score never clears 0.8? That’s where a dual-exit design helps.

The generate function fakes steady improvement — each round adds 0.3 to the score. The route_refinement function checks two things: did the score pass the bar, OR did the loop hit the max? Two exits cover the happy path and the worst case.

python
class RefinementState(TypedDict):
    draft: str
    score: float
    iterations: int
    max_iterations: int

def generate(state: RefinementState) -> dict:
    iteration = state["iterations"] + 1
    score = 0.3 * iteration  # simulated improvement each round
    return {
        "draft": f"Draft v{iteration}",
        "score": score,
        "iterations": iteration,
    }

def route_refinement(state: RefinementState) -> str:
    if state["score"] >= 0.8:
        return END
    if state["iterations"] >= state["max_iterations"]:
        return END
    return "generate"
python
refine_graph = StateGraph(RefinementState)
refine_graph.add_node("generate", generate)
refine_graph.add_edge(START, "generate")
refine_graph.add_conditional_edges("generate", route_refinement)

refine_app = refine_graph.compile()
result = refine_app.invoke({
    "draft": "",
    "score": 0.0,
    "iterations": 0,
    "max_iterations": 5,
})
print(f"Final draft: {result['draft']}")
print(f"Score: {result['score']}")
print(f"Iterations used: {result['iterations']}")
python
Final draft: Draft v3
Score: 0.9
Iterations used: 3

On round 3, the score hit 0.9 (0.3 × 3). That beat the 0.8 bar, so it stopped early. The max cap never fired — but it would have if the bar were out of reach.

Tip: Always pair a quality gate with a max-round guard. A quality check on its own can spin forever if the output never gets good enough. The max cap catches that edge case.

Exit Strategy 3 — How Do You Catch GraphRecursionError?

What about agents whose loop count is truly unpredictable? Catch the error at the call site and return a fallback. This works great in API endpoints and chatbot backends where an uncaught exception means a 500 error for the user.

python
from langgraph.errors import GraphRecursionError

def run_agent_safely(app, inputs, limit=25):
    try:
        result = app.invoke(inputs, {"recursion_limit": limit})
        return result
    except GraphRecursionError:
        return {
            "result": "I couldn't complete the task within "
                      "the step limit. Try a simpler question.",
            "error": "recursion_limit_exceeded",
        }

output = run_agent_safely(bad_app, {"value": 0}, limit=10)
print(output["result"])
python
I couldn't complete the task within the step limit. Try a simpler question.

Instead of a 500 error, the user gets a clear message. This is better than letting the exception bubble up — it gives you control over what the user sees.

Which Cycles Are Safe — and Which Are Dangerous?

Not all cycles carry the same risk. Here’s a quick cheat sheet.

Cycle Type Example Risk Why
Counter-bounded Retry up to 3 times Low Hard cap forces an exit
Quality-gated + cap Refine until score > 0.8, max 5 Low Two exit paths cover all cases
LLM-decided Agent calls tools until it decides to stop Medium LLM might never decide to stop
No exit at all Node always routes back to itself Critical Guaranteed crash
Hidden multi-node cycle A → B → C → A with no exit High Easy to miss in big graphs

The bottom line: if a human wrote the exit check with a hard cap, you’re safe. If the LLM picks when to stop and there’s no backup cap, you’re gambling.

Warning: LLM-driven loops need a backup cap. LLMs sometimes call the same tool over and over with slightly different inputs, hoping for a different result. Always add a max_iterations check next to the LLM’s own decision so you can cut the loop short.

Full Example — How Do You Build a Real Task Processing Loop?

Let’s tie all three safety layers together: a state counter, a quality check (all tasks done), and recursion_limit at the call site.

The agent works through a list of tasks. Each pass handles one task. The work_on_task node grabs the first unfinished task, marks it done, and bumps the counter. When nothing is left, it flips status to "all_done". The route_tasks function checks three things before choosing to loop or exit.

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

class TaskState(TypedDict):
    tasks: list[str]
    completed: list[str]
    iterations: int
    max_iterations: int
    status: str

def work_on_task(state: TaskState) -> dict:
    remaining = [t for t in state["tasks"] if t not in state["completed"]]
    if not remaining:
        return {"iterations": state["iterations"] + 1, "status": "all_done"}
    current = remaining[0]
    new_completed = state["completed"] + [current]
    return {
        "completed": new_completed,
        "iterations": state["iterations"] + 1,
        "status": f"completed: {current}",
    }

def route_tasks(state: TaskState) -> str:
    if state["status"] == "all_done":
        return END
    if state["iterations"] >= state["max_iterations"]:
        return END
    if len(state["completed"]) >= len(state["tasks"]):
        return END
    return "work_on_task"

Compile and run with three tasks.

python
task_graph = StateGraph(TaskState)
task_graph.add_node("work_on_task", work_on_task)
task_graph.add_edge(START, "work_on_task")
task_graph.add_conditional_edges("work_on_task", route_tasks)

task_app = task_graph.compile()

result = task_app.invoke({
    "tasks": ["fetch_data", "clean_data", "train_model"],
    "completed": [],
    "iterations": 0,
    "max_iterations": 10,
    "status": "",
}, {"recursion_limit": 15})

print(f"Completed: {result['completed']}")
print(f"Iterations: {result['iterations']}")
print(f"Status: {result['status']}")
python
Completed: ['fetch_data', 'clean_data', 'train_model']
Iterations: 4
Status: all_done

Four rounds: one per task, plus a last pass that found nothing left and flipped status to "all_done". The router caught the flag and stopped.

What Are the Most Common Cycle Mistakes?

Mistake 1 — The node doesn’t update the field the router checks

This is the number one cause of infinite loops.

Wrong:

python
def my_node(state: MyState) -> dict:
    return {"result": "done"}  # never updates 'count'

def should_stop(state: MyState) -> str:
    if state["count"] >= 3:  # count never changes!
        return END
    return "my_node"

Why it breaks: The router reads count, but the node never touches it. The check count >= 3 stays false forever.

Right:

python
def my_node(state: MyState) -> dict:
    return {"result": "done", "count": state["count"] + 1}

Mistake 2 — The recursion limit is too tight for multi-node cycles

Wrong:

python
# Graph has 5 nodes per cycle, needs ~15 steps
result = app.invoke(inputs, {"recursion_limit": 10})

Why it breaks: Each cycle pass uses 5 steps. Two passes eat 10 steps. One more node run and you hit the limit on a perfectly valid run.

Right:

python
# 5 nodes x 3 expected passes = 15, plus buffer
result = app.invoke(inputs, {"recursion_limit": 25})

Mistake 3 — No error handling in production

Wrong:

python
result = app.invoke(user_input)  # unhandled crash
return {"response": result["answer"]}

Right:

python
from langgraph.errors import GraphRecursionError

try:
    result = app.invoke(user_input, {"recursion_limit": 30})
    return {"response": result["answer"]}
except GraphRecursionError:
    return {"response": "I need more steps to answer this."}

Summary

Cycles give LangGraph agents the power to reason step by step. They drive ReAct loops, draft-and-refine flows, and page-by-page data collection. But a cycle with no exit will crash your agent every time.

Guard yourself with three layers:

  • State-based exit checks — counters or quality bars in your routing function

  • recursion_limit — a hard cap you set when you call invoke(), acting as a crash guard

  • GraphRecursionError handling — a try/except at the call site so users never see a raw stack trace

Use all three in production. The state check handles the normal path. The recursion limit catches your bugs. The error handler makes sure users get a clear message instead of a crash.

Practice exercise: Build a research agent loop. The agent starts with a question, runs a fake “search” (simulated), and scores its confidence (starts at 0.1, gains 0.2 per round). It stops when confidence hits 0.85 or after 5 rounds. Print the final confidence and round count.

Solution

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

class ResearchState(TypedDict):
    question: str
    confidence: float
    iterations: int
    max_iterations: int
    findings: str

def search(state: ResearchState) -> dict:
    iteration = state["iterations"] + 1
    confidence = min(0.2 * iteration + 0.1, 1.0)
    return {
        "iterations": iteration,
        "confidence": confidence,
        "findings": f"Found {iteration} sources",
    }

def route_research(state: ResearchState) -> str:
    if state["confidence"] >= 0.85:
        return END
    if state["iterations"] >= state["max_iterations"]:
        return END
    return "search"

research_graph = StateGraph(ResearchState)
research_graph.add_node("search", search)
research_graph.add_edge(START, "search")
research_graph.add_conditional_edges("search", route_research)

research_app = research_graph.compile()
result = research_app.invoke({
    "question": "How does photosynthesis work?",
    "confidence": 0.0,
    "iterations": 0,
    "max_iterations": 5,
    "findings": "",
}, {"recursion_limit": 10})

print(f"Confidence: {result['confidence']}")
print(f"Iterations: {result['iterations']}")
print(f"Findings: {result['findings']}")

Confidence reaches 0.9 on round 4 (0.2 × 4 + 0.1 = 0.9, which clears 0.85). The loop exits after 4 rounds, before hitting the max of 5.

Complete Code

Click to expand the full script (copy-paste and run)

python
# Complete code from: LangGraph Cycles, Recursion Limits, and Controlling Agent Loops
# Requires: pip install langgraph
# Python 3.10+

from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict
from langgraph.errors import GraphRecursionError

# --- Section 1: Simple Cycle ---
class CounterState(TypedDict):
    count: int
    message: str

def increment(state: CounterState) -> dict:
    new_count = state["count"] + 1
    return {"count": new_count, "message": f"Step {new_count}"}

def should_continue(state: CounterState) -> str:
    if state["count"] < 3:
        return "increment"
    return END

graph = StateGraph(CounterState)
graph.add_node("increment", increment)
graph.add_edge(START, "increment")
graph.add_conditional_edges("increment", should_continue)

app = graph.compile()
result = app.invoke({"count": 0, "message": ""})
print("Section 1 — Simple Cycle:")
print(result)
print()

# --- Section 2: GraphRecursionError ---
class LoopState(TypedDict):
    value: int

def loop_node(state: LoopState) -> dict:
    return {"value": state["value"] + 1}

def should_loop(state: LoopState) -> str:
    return "loop_node"

bad_graph = StateGraph(LoopState)
bad_graph.add_node("loop_node", loop_node)
bad_graph.add_edge(START, "loop_node")
bad_graph.add_conditional_edges("loop_node", should_loop)

bad_app = bad_graph.compile()

print("Section 2 — GraphRecursionError:")
try:
    bad_app.invoke({"value": 0}, {"recursion_limit": 5})
except GraphRecursionError as e:
    print(f"Caught it: {e}")
print()

# --- Section 3: State Counter Exit ---
class AgentState(TypedDict):
    query: str
    iterations: int
    max_iterations: int
    result: str
    done: bool

def agent_step(state: AgentState) -> dict:
    iteration = state["iterations"] + 1
    if iteration >= state["max_iterations"]:
        return {
            "iterations": iteration,
            "result": "Reached max iterations. Best answer so far.",
            "done": True,
        }
    return {
        "iterations": iteration,
        "result": f"Working... (iteration {iteration})",
        "done": False,
    }

def route_agent(state: AgentState) -> str:
    if state["done"]:
        return END
    return "agent_step"

agent_graph = StateGraph(AgentState)
agent_graph.add_node("agent_step", agent_step)
agent_graph.add_edge(START, "agent_step")
agent_graph.add_conditional_edges("agent_step", route_agent)

agent_app = agent_graph.compile()

print("Section 3 — State Counter Exit:")
result = agent_app.invoke({
    "query": "What is the capital of France?",
    "iterations": 0,
    "max_iterations": 3,
    "result": "",
    "done": False,
})
print(f"Iterations: {result['iterations']}")
print(f"Result: {result['result']}")
print()

# --- Section 4: Quality-Based Exit ---
class RefinementState(TypedDict):
    draft: str
    score: float
    iterations: int
    max_iterations: int

def generate(state: RefinementState) -> dict:
    iteration = state["iterations"] + 1
    score = 0.3 * iteration
    return {
        "draft": f"Draft v{iteration}",
        "score": score,
        "iterations": iteration,
    }

def route_refinement(state: RefinementState) -> str:
    if state["score"] >= 0.8:
        return END
    if state["iterations"] >= state["max_iterations"]:
        return END
    return "generate"

refine_graph = StateGraph(RefinementState)
refine_graph.add_node("generate", generate)
refine_graph.add_edge(START, "generate")
refine_graph.add_conditional_edges("generate", route_refinement)

refine_app = refine_graph.compile()
print("Section 4 — Quality-Based Exit:")
result = refine_app.invoke({
    "draft": "",
    "score": 0.0,
    "iterations": 0,
    "max_iterations": 5,
})
print(f"Final draft: {result['draft']}")
print(f"Score: {result['score']}")
print(f"Iterations used: {result['iterations']}")
print()

# --- Section 5: Real-World Task Loop ---
class TaskState(TypedDict):
    tasks: list[str]
    completed: list[str]
    iterations: int
    max_iterations: int
    status: str

def work_on_task(state: TaskState) -> dict:
    remaining = [t for t in state["tasks"] if t not in state["completed"]]
    if not remaining:
        return {"iterations": state["iterations"] + 1, "status": "all_done"}
    current = remaining[0]
    new_completed = state["completed"] + [current]
    return {
        "completed": new_completed,
        "iterations": state["iterations"] + 1,
        "status": f"completed: {current}",
    }

def route_tasks(state: TaskState) -> str:
    if state["status"] == "all_done":
        return END
    if state["iterations"] >= state["max_iterations"]:
        return END
    if len(state["completed"]) >= len(state["tasks"]):
        return END
    return "work_on_task"

task_graph = StateGraph(TaskState)
task_graph.add_node("work_on_task", work_on_task)
task_graph.add_edge(START, "work_on_task")
task_graph.add_conditional_edges("work_on_task", route_tasks)

task_app = task_graph.compile()

print("Section 5 — Task Loop:")
result = task_app.invoke({
    "tasks": ["fetch_data", "clean_data", "train_model"],
    "completed": [],
    "iterations": 0,
    "max_iterations": 10,
    "status": "",
}, {"recursion_limit": 15})

print(f"Completed: {result['completed']}")
print(f"Iterations: {result['iterations']}")
print(f"Status: {result['status']}")

print("\nScript completed successfully.")

Frequently Asked Questions

What’s the difference between recursion_limit and a custom max_iterations counter?

recursion_limit is LangGraph’s built-in safety net. It counts every node run and throws GraphRecursionError when the count is too high. A custom max_iterations is a field you add to your own state — you control the logic, and it triggers a clean exit through your routing function. Use both: max_iterations for the normal path, recursion_limit as the crash guard.

Can I set recursion_limit once at compile time?

Yes. Call graph.compile().with_config({"recursion_limit": 50}) to set a default for every run. You can still override it per call by passing a config to invoke().

Does recursion_limit count nodes or full cycle passes?

It counts individual node runs (called “supersteps”). If your cycle has 3 nodes and the limit is 25, you get about 8 full passes. Keep this in mind when your cycle spans many nodes.

What happens to state when GraphRecursionError fires?

Without a checkpointer, the in-progress state is gone. With checkpointing turned on, you can pull the last saved state and inspect what happened before the crash.

Do subgraphs share the parent’s recursion limit?

Yes. Every step inside a subgraph counts toward the parent’s recursion_limit. If the parent has a limit of 25 and a subgraph uses 10 steps, that leaves only 15 for the rest of the parent graph. Plan your limits with nesting in mind.

How do I stop an LLM from calling the same tool over and over?

Add a max_iterations counter to your state and check it in the router. When the count passes your cap, force the router to return END no matter what the LLM wants. This is the safest approach — you can’t rely on the LLM to stop on its own.

References

  • LangGraph documentation — GRAPH_RECURSION_LIMIT error reference. Link

  • LangGraph GitHub — Troubleshooting recursion limit errors. Link

  • LangGraph documentation — Interrupts for human-in-the-loop control. Link

  • LangGraph errors module — Python reference. Link

  • LangChain blog — Building LangGraph: Designing an Agent Runtime from first principles. Link

  • LangGraph official site — Agent Orchestration Framework. Link

  • Arxiv — Unsupervised Cycle Detection in Agentic Applications (2025). Link

Free Course
Master Core Python — Your First Step into AI/ML

Build a strong Python foundation with hands-on exercises designed for aspiring Data Scientists and AI/ML Engineers.

Start Free Course
Trusted by 50,000+ learners
Related Course
Master Gen AI — Hands-On
Join 5,000+ students at edu.machinelearningplus.com
Explore Course
Get the full course,
completely free.
Join 57,000+ students learning Python, SQL & ML. One year of access, all resources included.
📚 10 Courses
🐍 Python & ML
🗄️ SQL
📦 Downloads
📅 1 Year Access
No thanks
🎓
Free AI/ML Starter Kit
Python · SQL · ML · 10 Courses · 57,000+ students
🎉   You're in! Check your inbox (or Promotions/Spam) for the access link.
⚡ Before you go

Python.
SQL. NumPy.
All free.

Get the exact 10-course programming foundation that Data Science professionals use.

🐍
Core Python — from first line to expert level
📈
NumPy & Pandas — the #1 libraries every DS job needs
🗃️
SQL Levels I–III — basics to Window Functions
📄
Real industry data — Jupyter notebooks included
R A M S K
57,000+ students
★★★★★ Rated 4.9/5
⚡ Before you go
Python. SQL.
All Free.
R A M S K
57,000+ students  ★★★★★ 4.9/5
Get Free Access Now
10 courses. Real projects. Zero cost. No credit card.
New learners enrolling right now
🔒 100% free ☕ No spam, ever ✓ Instant access
🚀
You're in!
Check your inbox for your access link.
(Check Promotions or Spam if you don't see it)
Or start your first course right now:
Start Free Course →
Scroll to Top
Scroll to Top
Course Preview

Machine Learning A-Z™: Hands-On Python & R In Data Science

Free Sample Videos:

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science