Entry 016Guides8 min readBy Mandare Labs

How to stop an AI agent infinite loop: four layers

No single setting stops a runaway agent loop. A turn cap counts rounds, an SDK budget is an estimate made inside the agent's own process, a rate limit slows the loop without bounding its cost, and a spend cap enforced outside the process is the one that still holds when the agent code is wrong. Use all four, and make each stop final in the agent's code.

A runaway loop needs four stops that fail differently

An agent loop keeps going because every step looks reasonable from inside it. The stops that exist sit at four levels, and each one counts something different:

LayerCountsRuns inDoes not bound
Turn capRounds of the loopThe agent frameworkCost per round, time, work outside the loop
SDK budgetAn estimate of spendThe agent's processSpend the estimate misses, a restarted session
Rate limitRequests or tokens per minuteThe provider or a gatewayTotal spend over hours or days
Outside spend capMoney, per key or per mandateA gateway or the providerSpend that bypasses it

The sections below take them in that order, with the documentation for each. The last section covers what the agent's code should do when any of them trips, because a loop that retries a stop is a loop that has not stopped.

A turn cap counts rounds, not money

Every major agent framework has one. The OpenAI Agents SDK says of its runner loop: "If we exceed the max_turns passed, we raise a MaxTurnsExceeded exception." It adds: "Pass max_turns=None to disable this turn limit" (OpenAI Agents SDK, Running agents, read 16 October 2026). LangGraph calls its version a recursion limit and describes the failure as the graph having "reached the maximum number of steps before hitting a stop condition", which "is often due to an infinite loop". Its fix is a config value:

graph.invoke({...}, {"recursion_limit": 1000})

That line is from LangGraph's GRAPH_RECURSION_LIMIT page. Note the direction: the page says complex graphs "may hit the default limit naturally", so the documented remedy is to raise it. That is right for a graph that needs many steps and wrong for one that is looping, and the limit itself cannot tell which case it is.

The Claude Agent SDK documents max_turns in its Python reference as "Maximum agentic turns (tool-use round trips)", default None. Its agent loop page says the cap "counts tool-use turns only", and gives two edge cases that matter for runaway loops:

A turn cap is still a sound early stop. It is cheap, it needs no price table, and it ends the loop where the loop lives. It just says nothing about money.

An SDK budget is an estimate made inside the agent's process

Some SDKs add a spend threshold. The Claude Agent SDK's max_budget_usd is documented as: "Stop the query when the client-side cost estimate reaches this USD value. The estimate can pass this value, so leave headroom."

The agent loop page explains the headroom: the SDK compares spend with the cap "after model responses arrive", so "spend can therefore pass the cap by up to the cost of that one response", plus whatever subagents still running spend before they stop. The cost tracking page lists more edges:

A budget like this is a useful guard against a bug. It is a weak guard against a process that is wrong in some other way: a resumed session, a restart under a supervisor, or a second copy of the agent each get their own budget. Treat it as layer two, and note that it lives in the same process as the code it is meant to limit.

A rate limit changes how fast, not how much

Provider rate limits are often among the earliest limits a runaway loop meets, and they are easy to mistake for a cap. Anthropic's rate limit documentation says they "are measured in requests per minute (RPM), input tokens per minute (ITPM), and output tokens per minute (OTPM)". As of 16 October 2026 its Start tier table lists 1,000 requests per minute for every model it names.

Run the arithmetic. 1,000 requests a minute is 1,440,000 requests in 24 hours, and a loop that sits just under the limit never sees a 429. The same page describes the brake that does end it: a monthly spend cap per tier ($500 for Start, $1,000 for Build, as listed that day), or a lower limit you set on the Billing page. Two details from that page matter to loop handling:

Both are monthly, so a loop that burns a month's budget in a night is stopped after the money is gone. How to set a daily spending limit for an AI agent covers where a daily window exists and where it does not.

A rate limit does have one real use: it buys time. At 60 calls a minute a person can notice and react. At 1,000 they cannot.

A cap outside the process holds when the agent code is wrong

The fourth layer is a limit the agent cannot edit, restart or resume its way past. It can sit at the provider (the per-workspace spend limit above), in a gateway in front of it, or in both. A gateway budget has to be keyed to something that identifies the agent, and it has to decide what happens when the budget system itself is down. Two earlier entries compare how existing gateways answer those questions: LiteLLM budget per key and fail closed or fail open.

What this layer gives over the first three:

What it does not give: it covers just the traffic that passes through it. A tool that calls a paid API directly, with its own key, is not counted. The loop can also spin without spending, for example by re-running a local command forever. That is a turn-cap problem, not a spend problem.

Each stop looks different to the agent's code

The four layers report the stop in four different ways, and agent code that treats them all as "an error, try again" will loop through the very stops that were meant to end it.

StopWhat the caller seesRetry?
OpenAI Agents SDK turn capMaxTurnsExceeded exceptionNo: the run is over
Claude Agent SDK turn or budget capResult subtype error_max_turns or error_max_budget_usdNo: resume with a higher limit on purpose, not by retry
Anthropic tier spend capHTTP 429, enforced_spend_limit_reached, no retry-afterNo: fails until the reset
Anthropic rate limitHTTP 429, with a retry-after header when presentYes, after the wait

For the Claude Agent SDK, the loop page adds a trap: "A single-shot query() call yields the final result message, then raises an error that includes the failure text, such as Reached maximum number of turns. The raise is intentional." The page says to wrap the loop in a try block if your code needs to continue past it, and to branch on message.subtype for the reason. A blanket retry around that call would start a new run after the stop.

Two 429s that mean different things are the sharpest edge. One is a pause, the other is a wall. Read error.details.error_code before deciding.

Where Mandare fits

Mandare is an open-source gateway that applies the fourth layer, with a third layer beside it. The owner signs a mandate once, with caps per transaction, day, task and total, and the gateway ("the door") checks it before each call. The concepts page, Concepts, describes the mechanism: an intent "reserves its estimated cost inside the ledger transaction before the call", and the result settles the true cost.

The velocity limit is the rate layer. MANDARE_MAX_CALLS_PER_MINUTE defaults to 60 and is described as a "velocity floor" in the environment reference. Past it, the policy engine refuses with VELOCITY_EXCEEDED.

The runaway demo shows the spend layer. pnpm demo raises the velocity limit so the budget is the limit in play. The transcript below is copied from docs/demos/S2-runaway-demo.txt in the public repository (commit 327442e, 3 October 2026), with cuts marked:

pnpm demo (S2 runaway loop)mock provider
  agent=did:mandare:dev-agent caps: tx 5 / day 20 / total 100 EUR
…

  call # 72  403 DENIED — THE LOOP DIES HERE
    code:   PER_DAY_EXCEEDED
    reason: per-day budget exceeded: reserved 0.00 EUR + settled 19.723587 EUR + estimate 0.278853 EUR > cap 20.00 EUR
…
[demo] runaway made 71 calls in 0.1s before the mandate killed it.

The limits, stated next to the claim:

The same counts appear in a different form in a runaway agent loop dies at €20, which walks through the ledger verification that follows the refusal. The threat model lists what the cap does not cover.

Questions

How can an AI agent stuck in an infinite loop be stopped?

Set a turn limit in the framework (max_turns, recursion_limit), a spend limit in the SDK where one exists, and a spend cap in a layer the agent cannot edit, such as a gateway or the provider's own limit. The turn limit ends the loop in the framework, the outside cap bounds the money when the framework limit is missing or wrong.

Is max_turns enough to stop runaway cost?

No. It counts rounds of tool use, not money, and the context grows with every round, so a late turn costs more than an early one. The Claude Agent SDK documentation also says that in streaming input mode a queued message starts a new turn and the max-turns count starts over for it.

Does a provider rate limit stop a runaway loop?

It slows the loop, it does not end it. Anthropic's rate limits are measured in requests, input tokens and output tokens per minute, and its Start tier lists 1,000 requests per minute for each model it names. A loop at that rate stays inside the limit and keeps spending until a spend cap trips.

What should the agent do when a limit trips?

Stop, and report the reason. A refused call is a final answer, not a transient error. Retrying a spend-limit response fails by design: Anthropic's documentation says the spend-cap 429 has no retry-after header and that retries, including the SDK's automatic ones, fail until access resumes.

Sources

  1. How the agent loop works (Claude Agent SDK) · Anthropic
  2. Track cost and usage (Claude Agent SDK) · Anthropic
  3. Agent SDK reference, Python (ClaudeAgentOptions) · Anthropic
  4. Running agents (OpenAI Agents SDK for Python) · OpenAI
  5. GRAPH_RECURSION_LIMIT (LangGraph) · LangChain
  6. Rate limits (Claude API documentation) · Anthropic

Run it yourself: 3 commands, no API keys.

github.com/mandarelabs/mandare →
← All journal entries