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:
| Layer | Counts | Runs in | Does not bound |
|---|---|---|---|
| Turn cap | Rounds of the loop | The agent framework | Cost per round, time, work outside the loop |
| SDK budget | An estimate of spend | The agent's process | Spend the estimate misses, a restarted session |
| Rate limit | Requests or tokens per minute | The provider or a gateway | Total spend over hours or days |
| Outside spend cap | Money, per key or per mandate | A gateway or the provider | Spend 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:
- In streaming input mode, a message still queued when a turn ends at the limit starts a new turn, and "the max-turns count starts over for that turn".
- Context accumulates: "longer sessions with many tool calls build up significantly more context than short ones." A cap of 30 turns is not 30 equal charges: later turns carry more context than earlier ones.
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:
total_cost_usdis described as a client-side estimate, "not authoritative billing data", computed "from a price table bundled at build time".- The budget "counts only the call's own spend: totals restored from a resumed session don't count against it, and a
/clearstarts the budget over."
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:
- At the tier cap, requests return HTTP 429 with
error_codeenforced_spend_limit_reached, and "the response has noretry-afterheader. Retrying, including the SDK's automatic retries, fails until access resumes." - At a limit you set yourself, requests return HTTP 400 with type
invalid_request_error.
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:
- It does not depend on the agent's price table, its turn counter or its restart behaviour.
- It sees every call through it, from every copy of the agent and every resumed session, so one budget covers them all.
- It answers with a refusal the agent did not generate, which can be logged by something other than the agent.
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.
| Stop | What the caller sees | Retry? |
|---|---|---|
| OpenAI Agents SDK turn cap | MaxTurnsExceeded exception | No: the run is over |
| Claude Agent SDK turn or budget cap | Result subtype error_max_turns or error_max_budget_usd | No: resume with a higher limit on purpose, not by retry |
| Anthropic tier spend cap | HTTP 429, enforced_spend_limit_reached, no retry-after | No: fails until the reset |
| Anthropic rate limit | HTTP 429, with a retry-after header when present | Yes, 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:
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:
- This run uses a mock provider and needs no API keys: the enforcement is real, the money is not. The docker quickstart stops the same loop at call #24 with €19.17 settled, because it uses a pricier model.
- Caps hold given a correct price table. The environment reference says model ids "match exactly, so an unlisted variant is refused, never priced as a sibling". A model without a price is refused, not guessed.
- Reservations use an estimate, and the true cost settles from the provider's reported usage. Aborted streams settle at the estimate, never at zero (providers reference).
- The door proxies Anthropic
POST /v1/messagesand OpenAI or OpenRouterPOST /v1/chat/completions. Spend that does not pass through it is not counted. - This is version 0.1.0 of
@mandarelabs/clias of 16 October 2026. There is no external audit yet, and no hosted service.
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
- How the agent loop works (Claude Agent SDK) · Anthropic
- Track cost and usage (Claude Agent SDK) · Anthropic
- Agent SDK reference, Python (ClaudeAgentOptions) · Anthropic
- Running agents (OpenAI Agents SDK for Python) · OpenAI
- GRAPH_RECURSION_LIMIT (LangGraph) · LangChain
- Rate limits (Claude API documentation) · Anthropic
Run it yourself: 3 commands, no API keys.
github.com/mandarelabs/mandare →