A junior engineer It mentoreds last quarter was stuck. Their customer-support agent — meant to handle billing questions — had been working great in staging for two weeks. The moment they deployed it to production, it started getting stuck in infinite loops. Not on every conversation; on maybe 3% of them. By the time anyone noticed, the agent had made 11,000 tool calls overnight, burned through the entire monthly API budget, and the company's CFO was in their inbox asking for an explanation. It took a weekend of debugging to find the root cause. The fix was 12 lines of code. This article is about why agents loop, how to detect it, and how to stop it.
Why This Matters
Infinite loops are the silent killer of agent systems. They don't crash visibly. They don't produce errors. They just quietly burn compute, latency, and money while producing zero useful output. In a demo, they're rare and quickly noticed. In production, they slip past for hours or days before anyone catches them, and the financial damage can be severe. A single runaway agent can rack up thousands of dollars in API costs before being detected.
The phenomenon is also a great teacher. Most agent loops have identifiable root causes — confusion about task completion, missing termination conditions, oscillation between a few states, dependency on stale observations. Once you learn to recognize the patterns, you can prevent them by design rather than discovering them through a 3 AM page.
In 2026, the typical enterprise agent is handling dozens to hundreds of dollars of API costs per day per instance. A looping agent can hit thousands of dollars an hour. This isn't hypothetical; it's happening in production at scale. The teams that ship reliable agents have learned to engineer against loops. The teams that don't ship agents that occasionally take down a CFO's quarterly budget projection.
The Core Idea
There are essentially seven reasons agents loop. It will go through each, with the diagnostic signature and the fix.
1. No termination condition. The most basic cause. You wrote a loop, the agent does its work, but you never told it when to stop. The agent finishes its task but doesn't realize it's finished, so it keeps going. Diagnostic: trace shows the agent doing the same thing repeatedly with no progress. Fix: explicit termination in the system prompt ("Stop when you've completed these three steps and called finish[summary]") and a structured final action that the agent must take.
2. Unclear completion criteria. Even with a termination condition, the agent might not know whether it's met the criteria. "Look up the user's information" is vague — what counts as "looked up"? What if there are multiple records? What if the user is ambiguous? Diagnostic: agent calls the lookup tool multiple times for the same query with slight variations. Fix: precise criteria in the prompt ("Stop after calling lookup_user(external_id=...) and using the returned 'tier' field to determine the discount").
3. Tool errors that look like "try again." A tool returns an error and the agent interprets it as "It haven'ts succeeded yet, Trying again." This is particularly common with transient failures (network timeouts, rate limits, temporary unavailability). Diagnostic: the trace shows N identical tool calls in a row, often with the exact same arguments. Fix: structured error handling that says "If the tool returns this specific error code, do NOT retry — instead, surface it to the user or escalate."
4. Oscillation between two states. The agent discovers it can ping-pong between two actions that each create the precondition for the other. Classic example: agent updates a record, observes the update didn't take effect, looks up the record again, sees the old data, updates again. Diagnostic: alternating pattern in the trace (A, B, A, B, A, B). Fix: a state-tracking mechanism that prevents revisiting the same state within N steps, or a "have The configuration was tried this exact thing before" check.
5. Dependency on a tool that never returns the expected result. The agent is waiting for something that won't happen. A page that never loads, a record that doesn't exist, an API that returns 404 forever. Diagnostic: trace shows the agent repeatedly calling the same "check whether X is done" tool. Fix: timeout expectations in the prompt ("If the API returns 404, treat it as 'this record does not exist' and stop checking"), and explicit max attempts per tool.
6. The plan is wrong and the agent can't replan. The agent made a plan up front that doesn't work. Normally you'd expect plan-and-execute agents to detect this and replan. But many don't — they just keep executing the failed plan. Diagnostic: trace shows the agent executing the same planned steps over and over. Fix: a validation step after each sub-task that asks "Did this work? If not, replan."
7. The prompt encourages thoroughness at the expense of efficiency. A prompt that says "be thorough, check everything, leave no stone unturned" can produce agents that interpret every minor uncertainty as a reason to do another round of research. Diagnostic: the agent is doing way more work than needed for the task. Fix: explicit step limits ("Spend at most 3 tool calls on this section") and an instruction to terminate when the essential information is found.
A cross-cutting cause is missing state inspection. The agent has no way to look at its own history and recognize "It has already done this." Adding a "previous_actions" summary that's always present in the context can give the agent self-monitoring capability.
The other cross-cutting cause is missing budgets. Every loop above would have been terminated by a hard step or cost budget. Even if the agent is confused, even if the prompt is ambiguous, even if the plan is bad — a budget at the runtime level will eventually stop the loop. This is the single most reliable defense, and yet many production agents still don't have one.
A Concrete Example
Let's add loop-detection and hard budgets to a generic agent scaffold.
# safe_loop.py
from collections import deque
from dataclasses import dataclass
from typing import Callable
import hashlib
@dataclass
class StepRecord:
tool_name: str
args_hash: str
class LoopGuard:
"""Detect and prevent common agent loops."""
def __init__(self, max_repeats: int = 3, max_total_steps: int = 15):
self.recent: deque[StepRecord] = deque(maxlen=max_repeats * 2)
self.max_repeats = max_repeats
self.max_total_steps = max_total_steps
self.total_steps = 0
def check(self, tool_name: str, args: dict) -> tuple[bool, str]:
"""Returns (allowed, reason_if_blocked)."""
self.total_steps += 1
if self.total_steps > self.max_total_steps:
return False, f"Step budget exceeded ({self.max_total_steps})"
args_hash = hashlib.md5(
f"{tool_name}:{sorted(args.items())}".encode()
).hexdigest()
self.recent.append(StepRecord(tool_name, args_hash))
# Detect exact repetition: same tool + same args N times in a row
recent_same = [
r for r in list(self.recent)[-self.max_repeats:]
if r.tool_name == tool_name and r.args_hash == args_hash
]
if len(recent_same) >= self.max_repeats:
return False, (
f"Detected {self.max_repeats} identical calls to "
f"{tool_name} with the same arguments. Stopping."
)
# Detect A→B→A→B oscillation
if len(self.recent) >= 4:
last4 = list(self.recent)[-4:]
if (last4[0].tool_name == last4[2].tool_name
and last4[1].tool_name == last4[3].tool_name
and last4[0].args_hash == last4[2].args_hash
and last4[1].args_hash == last4[3].args_hash):
return False, "Detected oscillation between two tools. Stopping."
return True, ""
def run_with_guardrails(agent_fn: Callable, input_data: dict) -> dict:
"""Wrap an agent run with budgets and loop detection."""
guard = LoopGuard(max_repeats=3, max_total_steps=15)
cost = 0.0
steps = []
try:
# Drive the agent one step at a time, consulting the guard.
for step in agent_fn.iter_steps(input_data):
tool_name = step["tool_name"]
args = step["args"]
allowed, reason = guard.check(tool_name, args)
if not allowed:
return {
"status": "stopped_by_guard",
"reason": reason,
"steps": steps,
"cost_usd": cost,
}
result = step["execute"]()
steps.append({"tool": tool_name, "args": args, "result": result})
cost += result.get("cost_usd", 0)
except BudgetExceeded as e:
return {"status": "budget_exceeded", "reason": str(e), "cost_usd": cost}
return {"status": "completed", "steps": steps, "cost_usd": cost}
The key ideas:
- Step budget caps total tool calls.
- Exact-repeat detection catches "same tool, same args, N times in a row."
- Oscillation detection catches A→B→A→B patterns.
- Exception-based budget lets individual tools (expensive ones) enforce their own caps.
A real production system would also have an alert: when a stopped_by_guard event fires, log it, increment a metric, and notify the team. Loop incidents are diagnostic gold — they tell you exactly where your agent's design or prompt broke down.
Common Pitfalls
1. No step budget. The single biggest mistake. Even a generous budget (say 50 steps) is infinitely better than no budget. Set one from day one.
2. Detecting loops reactively instead of proactively. Some teams don't add loop detection until a loop has already cost them money. By then they've also lost user trust. Add detection from the start.
3. Relying solely on the model's self-control. "Just trust the model to do the right thing" is how you end up with 11,000 tool calls overnight. Runtime guards are not optional in production.
4. Alerts that no one reads. Setting up an alert for loop events is half the work. The other half is having someone who responds to it. Make sure your observability has a clear owner.
5. Not learning from loop incidents. Every loop incident should produce a prompt fix, a guardrail addition, or a tool improvement. Without that feedback loop, you'll keep having the same incidents.
6. Treating every loop as the same kind of problem. Different loop patterns have different causes and different fixes. An exact-repeat loop usually means the agent doesn't realize it already tried this. An oscillation usually means there's no clear stopping criterion. A planner-induced loop usually means the plan can't adapt to reality. Each pattern has a different diagnostic signature in the trace, and rushing to fix the symptom without understanding the cause produces brittle solutions that don't generalize.
7. Setting alerts on the agent loop but not on the underlying rate. Some teams set alerts when the agent enters a loop but miss that the entire system is in a slow loop — every customer-support ticket triggers an agent that triggers an external system that triggers another agent. Set alerts at the system level too. Look for any pair of services calling each other with suspicious frequency; that's where the real money-pits live.
When to Use This (And When Not To)
Loop detection and budgets apply to every production agent, full stop. There is no scenario in 2026 where shipping an agent without them is acceptable.
The sophistication of your loop detection scales with stakes. A toy agent can get away with just a step count. A production agent that handles money needs step count, cost budget, exact-repeat detection, oscillation detection, and human escalation when guards trigger.
There's also an interaction with streaming: if you stream partial responses to a UI, you might want to surface guard actions to the user ("It is encountering a repetitive situation and need to escalate this to a human"). This is a UX win that most teams miss.
Wrapping Up
Loops are inevitable — they will happen in your agent. The question isn't whether, but how soon, and whether you'll catch them before they cause damage. Hard budgets, runtime loop detection, and structured alerting are the three layers that prevent loops from becoming incidents. Build them into the agent from day one, not after the first $5,000 overage.
A useful framing as you think about loops: they're a symptom, not a disease. The disease is usually one of the seven underlying causes It walkeds through — unclear completion criteria, no termination condition, prompt ambiguity, tool errors that look like "try again," oscillation, dependency on a tool that never returns the expected result, or a plan that can't replan. The budgets and detection mechanisms are the cure, but the long-term fix is to address the root cause. Every loop incident is a feedback signal: it tells you exactly where your agent's design or prompt is broken. Treat loop incidents that way — triage, identify the cause, fix the prompt or the design, add a regression test. Over time, your loop rate will drop because you've systematically addressed the underlying causes.
The action item this week: pick one of your agents in production and add a step budget if it doesn't have one. Then add a basic exact-repeat detector. Total time: about an hour. Total benefit: a quiet weekend where no one has to debug an API bill. Then, when the next loop incident happens — and it will — don't just fix the loop. Fix the root cause that produced it.
Further Reading
Hermes Smith
