Token budgeting for agents means putting hard limits around the loop, not around the model. An agent that retries a failing tool call will happily do it sixty times, and every retry resends everything that came before. I wrote a small guard, ran it against a stub agent that loops on purpose, and the unguarded run burned $10.12 of list-price tokens on a task that never advanced. The guarded run stopped at $0.06.
Everything numeric below is output from scripts I ran on 2026-10-09. No API key was involved: the "agent" is a deterministic stub, so the dollar figures use Anthropic's published Sonnet 5.5 prices ($2 per million input tokens, $10 per million output tokens, from the pricing page that day). For the per-request arithmetic, see LLM API cost per 1,000 requests. For the cheaper-by-default levers, see the Claude API bill checklist.
Why do runaway loops get expensive so fast?
Because input cost is not linear in steps. Each turn sends the system prompt, the task and every previous tool result. The stub agent in my script starts with 1,500 tokens of context and adds 350 tokens of its own output plus 2,400 tokens of error text per failed tool call. That is a realistic shape: a stack trace or a long HTML error page is exactly what a failing tool returns.
After 60 steps the context is about 165,000 tokens. The sum of all the inputs sent along the way is what you pay for:
plain input tokens: 4,957,500 cost $10.12
with perfect prefix caching of the growing history: $1.10
The second line is a second small script. It assumes the growing history is a prefix cache hit on every step (reads at $0.10 per million, new tokens written at the 5-minute rate of $2.50) and it is a best case, not what you should budget on. Caching helps a lot, which is why the prompt caching post matters for agents. But it turns a $10 mistake into a $1 mistake, not into nothing. You still want the loop to stop.
What should a budget guard check?
Five different limits, because they fail differently:
| Limit | Catches | Misses |
|---|---|---|
| Step count | Any infinite loop | Slow, expensive single steps |
| Projected dollars | Large contexts, big outputs | Cheap loops that waste time |
| Wall-clock seconds | Slow tools, retries with sleeps | Fast loops |
| Repeated identical tool call | The classic stuck retry | Loops that vary the arguments |
| Context-size ceiling | Unbounded history growth | Short loops |
Check the dollar cap before the call using the projected cost, not after. If your cap is $0.25 and you have spent $0.24, the next call with 60,000 tokens of context should not happen.
The guard, in full
This is the code I ran (Python 3 standard library only). It tracks usage you feed it from each response, and it raises an exception with a snapshot when any limit trips.
import hashlib, json, time
PRICE_IN, PRICE_OUT = 2.00, 10.00 # Sonnet 5.5, USD per MTok, 2026-10-09
class BudgetExceeded(Exception):
def __init__(self, reason, snapshot):
super().__init__(reason)
self.reason, self.snapshot = reason, snapshot
class BudgetGuard:
def __init__(self, max_usd, max_steps, max_seconds, max_repeats=3, max_context_tokens=60_000):
self.max_usd, self.max_steps = max_usd, max_steps
self.max_seconds, self.max_repeats = max_seconds, max_repeats
self.max_context_tokens = max_context_tokens
self.usd = 0.0; self.steps = 0
self.in_tokens = 0; self.out_tokens = 0
self.started = time.monotonic(); self.recent = {}
def snapshot(self):
return dict(steps=self.steps, usd=round(self.usd, 4),
in_tokens=self.in_tokens, out_tokens=self.out_tokens)
def before_call(self, context_tokens, expected_out):
projected = self.usd + (context_tokens * PRICE_IN + expected_out * PRICE_OUT) / 1e6
if self.steps >= self.max_steps:
raise BudgetExceeded(f"step limit {self.max_steps}", self.snapshot())
if projected > self.max_usd:
raise BudgetExceeded(f"projected cost ${projected:.4f} > cap ${self.max_usd:.2f}", self.snapshot())
if time.monotonic() - self.started > self.max_seconds:
raise BudgetExceeded(f"time limit {self.max_seconds}s", self.snapshot())
if context_tokens > self.max_context_tokens:
raise BudgetExceeded(f"context {context_tokens} tokens > {self.max_context_tokens}", self.snapshot())
def after_call(self, usage):
self.steps += 1
self.in_tokens += usage["input_tokens"]
self.out_tokens += usage["output_tokens"]
self.usd += (usage["input_tokens"] * PRICE_IN + usage["output_tokens"] * PRICE_OUT) / 1e6
def check_tool_repeat(self, name, args):
key = hashlib.sha1((name + json.dumps(args, sort_keys=True)).encode()).hexdigest()
self.recent[key] = self.recent.get(key, 0) + 1
if self.recent[key] > self.max_repeats:
raise BudgetExceeded(f"same tool call repeated {self.recent[key]}x: {name}", self.snapshot())
The agent loop calls before_call with the current context size, makes the model call, feeds the usage back through after_call, and runs check_tool_repeat on every tool request. The loop wrapper catches BudgetExceeded and returns a partial result.
The stub agent and the runs
The stub never calls a model. In good mode it searches three times and finishes at step 4. In loop mode it asks for the same fetch_report call with the same arguments forever, and each failure adds 2,400 tokens to the context. The guard under test allows $0.25, 15 steps, 60 seconds, 3 repeats and a 60,000 token context.
Real output:
A. healthy task, no guard steps= 4 in= 12300 out= 1170 cost=$0.0363 -> finished normally
B. healthy task, guard on steps= 4 in= 12300 out= 1170 cost=$0.0363 -> finished normally
C. runaway loop, NO guard (demo stops at 60) steps=60 in=4957500 out=21000 cost=$10.1250 -> hard_stop=60 reached (demo safety net)
D. runaway loop, guard on steps= 4 in= 22500 out= 1400 cost=$0.0590 -> STOPPED: same tool call repeated 4x: fetch_report
E. runaway, repeat check off, cap $0.25 steps= 8 in= 89000 out= 2800 cost=$0.2060 -> STOPPED: projected cost $0.2570 > cap $0.25
F. runaway, only context ceiling 30k tokens steps=11 in= 167750 out= 3850 cost=$0.3740 -> STOPPED: context 31750 tokens > 30000
What each line tells you:
- A and B are identical, which is what you want. A guard that changes healthy runs is a bug.
- C is the cost of no guard. The "60" is my demo's own safety net; a real loop has no such net.
- D stops at step 4 on the repeat detector, at $0.059. That is the cheapest catch, because a stuck retry is the most common loop.
- E turns the repeat detector off, so the dollar cap has to work. It refuses the ninth call because the projected total, $0.257, would pass $0.25. Actual spend stayed at $0.206.
- F loosens the dollar and step caps and relies on the context ceiling, which stopped it at 31,750 tokens. Note the cost: $0.374, above the $0.25 of E. A context ceiling is a backstop, not a budget.
One limit of this design: the guard only knows about spend after a call returns, plus a projection before. A single call can still run long. Set max_tokens on every request so one response cannot blow through your cap.
How do you wire it to the real API?
Unrun, because I did not call the API in this session. The shape with the Anthropic Python SDK:
import anthropic
client = anthropic.Anthropic()
guard = BudgetGuard(max_usd=0.50, max_steps=20, max_seconds=120)
messages = [{"role": "user", "content": task}]
try:
while True:
# count_tokens or a running estimate of what you are about to send
guard.before_call(context_tokens=estimate_tokens(messages), expected_out=1000)
resp = client.messages.create(
model="claude-sonnet-5-5", max_tokens=1500,
tools=TOOLS, messages=messages,
)
guard.after_call({
"input_tokens": resp.usage.input_tokens + (resp.usage.cache_read_input_tokens or 0)
+ (resp.usage.cache_creation_input_tokens or 0),
"output_tokens": resp.usage.output_tokens,
})
if resp.stop_reason != "tool_use":
break
for block in resp.content:
if block.type == "tool_use":
guard.check_tool_repeat(block.name, block.input)
messages = run_tools_and_append(messages, resp) # your code
except BudgetExceeded as e:
log_and_return_partial(e.reason, e.snapshot, messages)
Two things to adapt. First, my after_call prices every input token at the plain rate; if you cache, price cache_read_input_tokens and cache_creation_input_tokens at their own rates from the pricing page, or your guard over-reports and trips early. The pricing multipliers are 1.25 times input for a 5-minute write and 0.05 times input for a read on Sonnet 5.5, per the pricing page that day. Second, estimate_tokens is yours to write (I did not check Anthropic's token counting docs for this post, so look there or use the previous response's usage as an estimate); token counting and context management covers the options.
What about Anthropic's task budgets?
Anthropic's docs list a beta feature called task budgets, which I read on 2026-10-09. It lets you give the model an advisory token budget for a full agentic loop, set as task_budget inside output_config with a beta header. The model sees a running countdown and is meant to wrap up gracefully as it runs out. Facts from that page that matter for this post:
- It is advisory. The page says it is a soft hint, not a hard cap, and Claude may occasionally exceed it. The enforced limit is still
max_tokensper request. - The minimum total is 20,000 tokens.
- The response does not report remaining budget. To track spend, you sum usage yourself, which is what the guard does.
- The page warns that mutating
remainingon every request invalidates cache prefixes containing it, so set it once. - It is listed as supported on several models in beta, including Opus 5.5, Sonnet 5.5 and Haiku 5.5, and not supported on Sonnet 5 or Claude Code. Check the page for the current list and header name, since it is a beta.
My read, not the docs': use task budgets so the model stops gracefully with a summary instead of mid-action, and keep the client-side guard as the hard stop. They do different jobs.
Choosing the numbers
I have no universal cap to give you, and I would distrust one. Measure instead:
- Run your agent on 30 to 50 real tasks with no cap, logging steps, tokens and dollars per task.
- Look at the distribution. Set the step cap just above the 95th percentile and the dollar cap at two to three times the median successful cost. That is a rule of thumb of mine, not a measured optimum.
- Set the repeat limit low (3 is a reasonable start) and add arguments that legitimately repeat to an allowlist.
- Put a context ceiling well below the model's window, so history compaction or summarising happens before you hit it. See context compaction for long-running agents and what to include and cut from the context window.
- Alert when the guard trips. Review each trip for the first month.
If you run many agents behind a gateway, budgets can also live there, per key and per team. That is the subject of the LiteLLM budgets post. If agents are new to you, the agents track explains the loop this post puts a fence around. For the loop structure itself, see plain Python agent loop versus a framework, and the agent production checklist for the rest of what to do before shipping.
What I did not test
The guard ran on a stub, so I have not shown it stopping a real model. Real agents also vary arguments slightly on each retry, which defeats exact-match repeat detection; you may need to hash only the tool name plus a normalised key argument. The costs use list prices and ignore taxes, gateway markups and retries inside the SDK. Treat the numbers as the shape of the problem, and rerun the script with your own context growth.



