Token Budgets Per Request: How to Stop Your Agent From Bankrupting a Feature
One runaway agent loop can eat a week of margin. Here's how we set per-request token budgets, enforce them at the SDK layer, and keep product features profitable without lobotomising the model.

The first time an agent we shipped hit a tool-calling loop in production, it burned through about a month's projected inference budget in a single afternoon. No crash, no alert — just a very patient model politely re-planning itself into oblivion. Since then, every agent we build gets a hard token budget per request before it gets a system prompt.
This is the pattern we wish someone had handed us two years ago.
Why per-request budgets, not just monthly caps
Vendor dashboards give you monthly spend caps. Those are useful for accounting and roughly useless for engineering. By the time a monthly cap trips, you've already served thousands of degraded requests, and the cap kills everyone's traffic, not just the pathological ones.
What you actually want is a budget that lives at the same scope as a user action:
- A single chat turn
- A single background job
- A single agent "task" (which may involve many tool calls)
The budget is denominated in tokens, but it maps cleanly to money. If your average blended cost on Claude Sonnet is roughly $3–$15 per million tokens depending on input/output mix (see Anthropic's pricing page for current numbers), a 50k-token budget per request puts a hard ceiling of a fraction of a cent to a few cents on that request. Multiply by your traffic, and you can finally answer the question product keeps asking: what does this feature cost per user per month?
The three failure modes a budget prevents
- Runaway loops. An agent that keeps calling
searchthenplanthensearchagain because a tool returned ambiguous output. - Context bloat. Each turn appends the previous turn's tool outputs. Without pruning, a 10-turn conversation can carry 200k tokens of stale JSON.
- Prompt injection amplification. A malicious document tells the model to "summarise all previous documents in full". Without a budget, it will try.
Sizing the budget
We start from unit economics, not from the model's context window. The question is not "how much can Gemini 2.5 fit?" — it's "how much can this feature afford?"
A rough working formula:
budget_tokens = (target_cost_per_request / blended_price_per_token) * safety_factor
Where safety_factor is usually 0.7 — you want headroom for the occasional legitimately hard request. If a feature needs to cost under $0.02 per call and your blended price is $8/M tokens, that's a budget around 1,750 tokens. Which sounds tiny, until you realise most chat turns fit comfortably.
We usually define three tiers per feature:
- Soft budget (e.g. 60% of hard): log a warning, emit a metric
- Hard budget (100%): stop the agent, return best-effort output
- Kill budget (150%): raise, page on-call if this fires more than N times per hour
The soft budget is what makes this useful in practice. It gives you a leading indicator that a prompt change is drifting expensive before it starts failing.
Enforcing it in code
Most SDKs don't give you a first-class "stop at N tokens" primitive across a whole agent run. You have to build it. Here's the shape we use — a thin wrapper around whichever provider we're on that tracks a running total and short-circuits the loop.
type BudgetState = {
hardLimit: number;
softLimit: number;
spent: number;
onSoft?: () => void;
};
export class TokenBudget {
constructor(private state: BudgetState) {}
charge(inputTokens: number, outputTokens: number) {
this.state.spent += inputTokens + outputTokens;
if (
this.state.spent >= this.state.softLimit &&
this.state.spent - inputTokens - outputTokens < this.state.softLimit
) {
this.state.onSoft?.();
}
}
assertUnder() {
if (this.state.spent >= this.state.hardLimit) {
throw new BudgetExceeded(this.state.spent, this.state.hardLimit);
}
}
remaining() {
return Math.max(0, this.state.hardLimit - this.state.spent);
}
}
The agent loop then looks like this:
async function runAgent(task: string, budget: TokenBudget) {
const messages: Message[] = [{ role: 'user', content: task }];
for (let step = 0; step < MAX_STEPS; step++) {
budget.assertUnder();
const response = await model.complete({
messages,
tools,
max_tokens: Math.min(2048, budget.remaining()),
});
budget.charge(response.usage.input_tokens, response.usage.output_tokens);
if (response.stop_reason === 'end_turn') return response;
const toolResults = await runTools(response.tool_calls);
messages.push(response.message, ...toolResults);
}
}
Two details that matter more than they look:
max_tokensis clamped tobudget.remaining(). This is what prevents the last call in a nearly-exhausted budget from over-shooting by 4k tokens.assertUnderruns before the next call, not after. Once you've spent the money, throwing doesn't refund it. Check first.
Handling the hard-limit throw gracefully
A BudgetExceeded should not look like a 500 to the user. Catch it at the feature boundary and return whatever partial state the agent had. For a research agent, that might be "here's what I found so far, I stopped before completing step 4". For a code-writing agent, it might be the current draft plus a note.
Users tolerate truncation far better than they tolerate spinners that die.
Where the budget number comes from
We don't guess. We run an eval set — the same one we use for regression testing — with cost tracking on, and look at the distribution.
| Percentile | Tokens used |
|---|---|
| p50 | 8,400 |
| p90 | 22,100 |
| p95 | 31,800 |
| p99 | 74,500 |
The hard budget goes somewhere between p95 and p99. Below p95, you'll reject too many legitimate requests. Above p99, you're paying for the tail. In our experience, p97 is a sensible starting point, then you tune based on how often the soft budget fires in prod.
Re-run this every time you change the system prompt, add a tool, or swap models. Prompt changes can shift the p95 by 30% in either direction without touching the p50.
Model choice interacts with the budget
A budget expressed in tokens hides a real tradeoff: cheaper models often need more tokens to reach the same answer, because they lean harder on tool calls and self-correction. We've seen tasks where switching from a top-tier model to a mid-tier one saved 60% on per-token price but increased token usage by 2.5x — net loss.
Some rough heuristics from projects we've shipped:
- For structured extraction with clear schemas, smaller models (Haiku, GPT-4o mini, Gemini Flash) usually stay well inside budget.
- For multi-step agents with ambiguous tools, frontier models typically finish in fewer steps and end up cheaper despite the higher sticker price.
- For long-document Q&A, prompt caching (documented by Anthropic and OpenAI) changes the maths entirely — the input side of your budget can drop by 80%+ on cache hits.
Benchmark this on your workload before committing. Vendor comparison tables won't tell you how your specific agent behaves.
What to log
Every agent run should emit, at minimum:
budget_hard,budget_spent,budget_soft_tripped- Tokens per step (so you can see which tool call blew up)
stop_reasonincluding your ownbudget_exceeded- Model, prompt version, feature ID
With that in a warehouse, you can answer questions like "which prompt version has the fattest tail?" and "is feature X's p95 creeping up week over week?" Those are the questions that catch cost regressions before they show up on the invoice.
Where we'd start
If you're retrofitting this into an existing agent, do it in this order:
- Instrument first. Add token counting to every model call and ship it to your logs. Don't enforce anything yet — just watch for a week.
- Set the hard budget at p99 of what you observe. This will almost never fire, but it'll catch the truly pathological cases immediately.
- Add the soft budget at p90 and wire it to a dashboard. Watch which features drift.
- Tighten from there. Once you trust the metric, pull the hard budget down toward p97, then p95, and negotiate with product about what to do when it trips.
The goal isn't to make the model cheap. It's to make the worst case predictable, so you can ship features with confidence instead of hoping nobody asks the agent to do something silly on a Friday afternoon. If you want a hand pressure-testing this on a real workload, our team does this work as part of our AI engineering services.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Eval Harnesses That Catch Regressions Before Users Do
Most teams write prompts, ship, and pray. Here's how we build eval harnesses that actually catch regressions before a model swap or prompt tweak breaks production.

Long-Context Windows vs RAG: When 1M Tokens Actually Beats Retrieval
Gemini and Claude now ship million-token windows. That doesn't mean you should stuff everything into the prompt. Here's how we decide between long context and RAG on real projects.

Prompt Caching in Production: What Actually Saves Money vs What Just Looks Clever
Prompt caching sounds like free money — until your hit rate collapses at 3am. Here's what we've learned shipping cached prompts across Claude, OpenAI, and Gemini, and where the savings actually come from.
