AI Agent Stuck in an Infinite Loop? Here’s How to Stop It (and the Bill)

AI agent stuck in an infinite loop with rising API costs

Somewhere out there, right now, an AI agent is calling the same tool for the ninth time in a row, absolutely convinced that the tenth call will finally give it a different answer.

It won’t.

If you’re reading this at 2 AM because a Slack alert just told you your OpenAI or Anthropic bill jumped by four figures overnight, I’m not going to waste your time with theory. I’m going to show you exactly why this happens and exactly how to stop it — today, in the next ten minutes, and then permanently.

This isn’t a rare edge case anymore. As agentic workflows have moved from weekend projects to production systems in 2026, “the agent looped forever” has become one of the most common — and most expensive — failure modes in backend engineering. One team on the LangChain forum described watching four agents run undetected for eleven days before the cloud invoice made it obvious. Every single API call had returned a healthy 200 OK. Nothing “crashed.” The system just never stopped.

That’s the part that makes this bug so dangerous: it doesn’t look like a bug. It looks like your agent working really, really hard.

What “Agent Infinite Loop” Actually Means

An AI agent loop happens when an LLM-powered agent keeps re-executing the same tool calls or reasoning steps without ever reaching a stopping point. Unlike a normal software bug that throws an error and dies, an agent loop is a polite failure — every individual step succeeds. The tool call works. The API responds. The agent just never decides it’s done.

You’ll usually meet this bug in one of two ways:

  1. A graceful stop that isn’t graceful at all — your framework hits max_iterations and halts, but only after burning through your entire step budget with zero progress.
  2. A hard crash — LangGraph throws GRAPH_RECURSION_LIMIT reached, or raw Python throws RecursionError: maximum recursion depth exceeded, because nothing was bounding the loop in the first place.

Either way, by the time you see the error, the damage — in tokens, in latency, in API spend — has already happened.

Why Your Agent Is Looping (The Real Root Causes)

Before you touch a single config value, it’s worth understanding why this happens. Bumping max_iterations without knowing the cause just means your agent fails slower and more expensively.

1. The tool’s description doesn’t tell the model when to stop.
This is the single most common cause, and it’s almost never a “reasoning failure” — it’s a documentation problem. If your tool’s docstring just says "Search the documentation", the model has no signal for when the result is good enough. It calls the tool, gets something ambiguous back, and reasons: this tool still seems relevant, let me try again.

2. The agent’s memory is lossy.
Frameworks that summarize scratchpad history to save context window space can accidentally hide the fact that a step already happened. The agent looks at a compressed version of its own past (“you called search, here’s a summary”) and re-derives a plan it already tried — because from where it’s sitting, it never tried it.

3. There’s no explicit “done” condition.
If the prompt never defines what success looks like, the agent never feels finished. It keeps reasoning because reasoning is what it does.

4. Retries on transient failures quietly eat the step budget.
A flaky API, a rate limit, a timeout — each retry counts as an iteration. Ten transient failures can burn ten steps before the agent has done any real work.

5. Routing logic in graph-based agents never reaches an exit node.
In LangGraph specifically, a should_continue() function that always routes back to the same node — and never to END — will cycle forever. If two nodes in your trace keep alternating with an identical state hash, this is your bug.

Emergency Fix: Stop the Bleeding in the Next 5 Minutes

If you have a runaway agent right now, do these two things first — they’re the circuit breaker, not the cure:

from langchain.agents import AgentExecutor

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    max_iterations=15,        # hard ceiling on reasoning steps
    max_execution_time=60,    # seconds — bounds latency AND cost
    early_stopping_method="generate",  # produce a best-effort answer instead of just dying
    return_intermediate_steps=True,    # so you can actually debug the trace afterward
)

For LangGraph, set an explicit recursion limit at invoke time rather than relying on the framework default:

result = graph.invoke(
    {"messages": [...]},
    config={"recursion_limit": 20}
)

A rule worth tattooing on your monitor: do not just 10x your max_iterations when you see this error. If the agent wasn’t making real progress in 15 steps, it won’t magically make progress in 150 — you’ll just pay for a bigger version of the same failure. Raise the limit modestly, and only after you understand why it looped.

The Permanent Fix: Give the Agent an Actual Exit Door

Emergency caps buy you time. They don’t fix the design flaw. Here’s what does.

1. Make tool descriptions carry the stop condition

from langchain_core.tools import tool

@tool
def search_docs(query: str) -> str:
    """Search internal documentation for the query.
    Returns up to 3 relevant snippets.
    If the returned snippets fully answer the question, STOP calling this tool
    and produce a final answer. Do not call this tool more than twice per query."""
    return vector_store.similarity_search(query, k=3)

Notice this isn’t a prompt change — it’s a tool-description change. The model reads tool descriptions on every single reasoning step, so this is the highest-leverage place to bake in a stop signal.

2. Add a hard per-tool call ceiling (defense in depth)

Even with a better description, put a deterministic guardrail in code — because you cannot trust an LLM to reliably decide when it’s done. This has to be enforced outside the model:

class ToolCallLimiter:
    """Hard ceiling on tool calls per agent invocation."""
    def __init__(self, max_tool_counts: dict[str, int]):
        self.max_tool_counts = max_tool_counts
        self.tool_counts = {}

    def reset(self):
        self.tool_counts = {}

    def check(self, tool_name: str) -> bool:
        count = self.tool_counts.get(tool_name, 0) + 1
        self.tool_counts[tool_name] = count
        limit = self.max_tool_counts.get(tool_name)
        if limit and count > limit:
            return False  # block the call, tell the model explicitly to stop
        return True

3. Fix the routing logic in graph-based agents

If you’re on LangGraph, audit every conditional edge in your StateGraph. Every branch needs a real path to END — not just a path back to another reasoning node:

def should_continue(state):
    if state["step_count"] >= state["max_steps"]:
        return "end"
    if state.get("task_complete"):
        return "end"
    return "continue"

workflow.add_conditional_edges(
    "agent",
    should_continue,
    {"continue": "agent", "end": END}
)

The step_count check matters more than it looks — it’s a deterministic counter that doesn’t depend on the model “deciding” anything.

4. Make the prompt state the budget explicitly

Give the model a visible, numeric budget instead of an open-ended instruction to “use your judgment”:

You have at most 8 tool calls for this task.
After each tool call, ask yourself: did this reduce uncertainty?
As soon as you can answer confidently, stop and output final JSON:
{"answer": "...", "sources": [...]}
Do not call any tool after producing this output.
If a tool fails twice in a row or returns no new information, stop and summarize the blocker instead of retrying.

Don’t Just Stop Loops — Watch the Money

Bounded loops still cost money if nobody’s watching the meter. A per-invocation cost tracker is cheap insurance:

class CostCircuitBreaker:
    def __init__(self, max_cost_usd: float):
        self.max_cost_usd = max_cost_usd
        self.spent = 0.0

    def track(self, cost: float):
        self.spent += cost
        if self.spent > self.max_cost_usd:
            raise RuntimeError(
                f"Cost circuit breaker tripped: ${self.spent:.2f} "
                f"exceeds ${self.max_cost_usd:.2f} limit for this task"
            )

Pair this with a budget alert at the account level too — but don’t rely on the account-level alert alone. Those typically fire on a delay, and a delay is exactly what a runaway agent doesn’t give you.

Quick Diagnostic Checklist

Before you touch any code, check the trace for these signs:

  • Same tool, same or near-identical arguments, called more than twice → ambiguous stop condition
  • AgentScratchpad length shorter than actual step count → lossy memory / summarization hiding repetition
  • Two nodes alternating with identical state hash in LangGraph trace → missing exit edge
  • Errors preceded by several retried steps that returned no new information → retries eating the step budget

Frequently Asked Questions

Why did my agent hit max_iterations even though I set it high?

A high max_iterations value doesn’t fix the underlying loop — it just delays the crash and increases the bill you’ll pay before it happens. Fix the root cause first, then set the cap as a safety net.

Is max_execution_time or max_iterations more important?

Use both. max_iterations bounds cost per invocation; max_execution_time bounds latency and protects against a small number of very slow, expensive steps slipping past your iteration cap.

Can the LLM ever reliably decide when to stop on its own?

No. Treat “the model will know when it’s done” as false by default. Deterministic, code-level guardrails — iteration caps, tool call limits, and cost circuit breakers — are what actually stop production incidents, not prompt wording alone.

My LangGraph agent throws RecursionError, not the graceful GRAPH_RECURSION_LIMIT error. What’s different?

That means the recursion limit itself wasn’t set, so Python’s own interpreter-level recursion limit fired instead — a hard crash rather than a controlled stop. Explicitly set recursion_limit in your graph config so you get the graceful LangGraph error instead of a raw Python crash.

Final Thought

An agent that loops isn’t broken in the way a crashing server is broken — it’s broken in the way a very diligent employee is broken when nobody ever told them the job was finished. Every fix above boils down to the same idea: don’t ask the model to know when to stop. Tell it, and back that up with code that enforces it regardless of what the model decides.

If you’re running agents in production without at least an iteration cap, a tool call limiter, and a cost circuit breaker, you don’t have an AI system — you have an open invoice with a language model attached to it.

For more on agent configuration, check the official LangChain and LangGraph documentation.

How to Fix Error listen EADDRINUSE address already in use :::3000 in Node.js
How to Fix MongoDB Atlas Connection Timeout Error in Node.js


Related reading on VoraWire: if you’re debugging LLM-related production issues, check out our guides on OpenAI API 429 rate limit handling and LangChain asyncio timeout errors for other common failure modes in AI-integrated backends.


Leave a Reply

Your email address will not be published. Required fields are marked *