teachyou.ai academy
← All posts
AI AgentsLLM cost optimizationtoken usageprompt cachingagent monitoring

Agent Cost Budgeting: How to Stop AI Agents From Burning Your Token Budget

Pramod Dutta · Jul 10, 2026 · 12 min read

Agent cost budgeting is the practice of putting hard limits, not just monitoring dashboards, around what an autonomous AI agent is allowed to spend before it runs a single task. Most teams find out they need this the hard way: a coding agent gets stuck in a retry loop, or a research agent keeps spawning sub-agents to "double check" a fact, and a task that should have cost a few cents burns through several dollars of tokens in minutes. Agent cost budgeting fixes this by treating cost the same way you treat latency or memory: as a resource with a ceiling, enforced in code, not in a spreadsheet you check after the fact.

This matters more for agents than for regular LLM calls because agents are non-deterministic in how much work they do. A single chat completion has one predictable cost. An agent loop might call a tool once or fifty times depending on what it finds, and every tool result gets fed back into the context window, which means every additional step is more expensive than the last one. Without budgeting, an agent's cost curve is unbounded by design.

Why Agent Cost Budgeting Is Different From LLM Cost Tracking

Regular LLM cost tracking answers "how much did we spend last month." Agent cost budgeting answers "how much is this specific task allowed to spend, right now, before it starts." The difference is enforcement timing.

Tracking is retrospective. You pull usage from your provider's dashboard or your own logs, sum it up, and notice a spike after it already happened. That's useful for finance reporting, but it does nothing to stop a single misbehaving agent run from draining your budget while it's in progress.

Budgeting is prospective and enforced inline. The agent framework checks, before or during execution, whether a task has already spent close to its allotment, and either stops, downgrades, or asks for human approval. This requires three things that plain tracking doesn't:

  • A per-task or per-session cost ceiling, decided before the agent starts
  • A running cost counter that updates after every model call and tool call
  • A circuit breaker that actually halts execution when the ceiling is hit

Agent frameworks that only expose token counts in a log file are giving you tracking, not budgeting. You have to build the enforcement layer yourself, and it's simpler than it sounds if you do it at the right layer: the loop that dispatches each model call, not the UI that displays results afterward.

How Agent Costs Actually Compound

Before you can set a sane budget, you need to understand where the money actually goes in an agentic loop. It's rarely the "main" reasoning call that dominates cost. It's usually one of these three multipliers.

Context growth per turn. Every tool call result, every file read, every intermediate reasoning step gets appended to the conversation history and resent to the model on the next turn. A ten-step agent loop doesn't cost ten times the price of one call, it costs closer to the sum of ten increasingly large context windows, because turn nine is paying for the tokens of turns one through eight all over again.

Sub-agent fan-out. Orchestrator patterns where a lead agent spawns multiple sub-agents to research or verify something in parallel multiply cost by the number of sub-agents, and each sub-agent has its own growing context. A "quick fact check" that spawns four sub-agents each doing three tool calls is not a cheap operation, even though every individual call looks small.

Retry and self-correction loops. Agents that check their own output and retry on failure (a good practice for correctness) can double or triple cost silently. If a coding agent runs a test, sees a failure, and retries five times before giving up, you paid for five attempts, not one.

The practical takeaway: budget at the level of the whole task, not the individual model call. A per-call limit doesn't catch fan-out or retries. A per-task limit does.

Setting a Budget Before You Write a Single Line of Agent Code

Pick numbers before you build anything. A good starting budget has three tiers:

  1. Soft warning threshold. Around 50-60% of the hard limit. Log it, maybe notify a Slack channel, but let the agent keep working.
  2. Hard stop threshold. 100% of the allotted budget. The agent must terminate the current task, return whatever partial result it has, and report why it stopped.
  3. Per-session ceiling. A separate, usually higher, limit that caps total spend across a user's entire session, in case they run many tasks in a row.

Decide these numbers based on the value of the task, not just what feels reasonable. A customer support agent answering a simple billing question should have a tiny budget, maybe a few cents worth of tokens. A coding agent doing a multi-file refactor across a large repo legitimately needs a much bigger allotment. Treating every agent task with the same budget either starves your expensive-but-valuable tasks or lets your cheap-but-frequent tasks bleed you dry at volume.

Tracking Token Usage Per Task in Code

Every major model API returns usage data on each response: input tokens, output tokens, and (increasingly) cached-token counts. The budgeting layer is just a running accumulator that reads this field after every call and compares it against the ceiling.

Here's a minimal cost tracker you can drop into an agent loop, written against the Claude API's usage fields but structurally the same across providers:

class BudgetExceeded(Exception):
    pass

class CostTracker:
    def __init__(self, max_usd, input_price_per_mtok, output_price_per_mtok):
        self.max_usd = max_usd
        self.input_price = input_price_per_mtok
        self.output_price = output_price_per_mtok
        self.spent_usd = 0.0
        self.calls = 0

    def record(self, usage):
        input_cost = (usage.input_tokens / 1_000_000) * self.input_price
        output_cost = (usage.output_tokens / 1_000_000) * self.output_price
        self.spent_usd += input_cost + output_cost
        self.calls += 1
        if self.spent_usd >= self.max_usd:
            raise BudgetExceeded(
                f"Task hit budget ceiling: ${self.spent_usd:.4f} "
                f"spent over ${self.max_usd} limit after {self.calls} calls."
            )
        return self.spent_usd

    def remaining(self):
        return max(0.0, self.max_usd - self.spent_usd)

Wire it into the loop where you dispatch calls to the model:

tracker = CostTracker(max_usd=0.50, input_price_per_mtok=3.0, output_price_per_mtok=15.0)

def run_agent_step(client, messages, model):
    response = client.messages.create(
        model=model,
        max_tokens=1024,
        messages=messages,
    )
    tracker.record(response.usage)
    return response

Note the prices here are placeholders you fill in from your provider's current published rates, not hardcoded numbers you should copy verbatim. Pricing changes, and per-model rates differ, so read them from a config file or your provider's pricing page rather than baking a number into the tracker itself.

Building a Circuit Breaker That Actually Stops the Loop

A tracker that raises an exception is only half the job. You need the agent's control loop to catch that exception and do something useful with a partially completed task instead of just crashing.

def run_agent_task(client, model, initial_prompt, tools, max_steps=20):
    messages = [{"role": "user", "content": initial_prompt}]
    for step in range(max_steps):
        try:
            response = run_agent_step(client, messages, model)
        except BudgetExceeded as e:
            return {
                "status": "budget_exceeded",
                "partial_messages": messages,
                "reason": str(e),
            }

        if response.stop_reason == "tool_use":
            tool_results = execute_tools(response.content, tools)
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})
        else:
            return {"status": "complete", "messages": messages}

    return {"status": "max_steps_reached", "messages": messages}

Two ceilings are doing work here: max_steps caps runaway loops even when each step is cheap, and the cost tracker catches the case where a small number of steps is individually expensive (large file reads, big tool outputs). You want both, because a loop can blow its budget in three expensive steps or twenty cheap ones, and each failure mode needs a different guard.

When the budget is exceeded mid-task, don't just discard the work. Return the partial transcript so a human (or a cheaper fallback model) can pick up where the agent left off. Throwing away partial progress on an already-paid-for task is the most wasteful outcome possible.

Prompt Caching Changes Your Budget Math

If your agent re-sends a large system prompt, a big tool schema list, or a repo's worth of file context on every turn, prompt caching is the single highest-leverage lever for cost budgeting. Cached input tokens are billed at a steep discount compared to fresh input tokens, so an agent loop with a stable system prompt and growing-but-mostly-repeated context can see costs drop sharply once caching kicks in on turn two and beyond.

The practical implication for budgeting: your cost tracker should read the cache-read and cache-write token fields separately from regular input tokens, because they're priced differently.

def record_with_cache(tracker, usage, input_price, output_price, cache_write_price, cache_read_price):
    cost = 0.0
    cost += (usage.input_tokens / 1_000_000) * input_price
    cost += (usage.output_tokens / 1_000_000) * output_price
    if getattr(usage, "cache_creation_input_tokens", 0):
        cost += (usage.cache_creation_input_tokens / 1_000_000) * cache_write_price
    if getattr(usage, "cache_read_input_tokens", 0):
        cost += (usage.cache_read_input_tokens / 1_000_000) * cache_read_price
    tracker.spent_usd += cost
    if tracker.spent_usd >= tracker.max_usd:
        raise BudgetExceeded(f"Budget hit at ${tracker.spent_usd:.4f}")

The structural lesson: put your large, stable content (system instructions, tool definitions, reference documents) at the front of the context and keep it identical across turns. Put the fast-changing content (the latest tool result, the newest user message) at the end. That ordering is what makes caching actually hit, and it's a design decision, not a config flag, so make it early rather than retrofitting it after your budgets are already blown.

Model Tiering: Downgrading Automatically Under Pressure

A budget that only stops agents dead is a blunt instrument. A better pattern is tiering: when an agent crosses the soft warning threshold, downgrade to a cheaper, faster model for the remainder of the task instead of halting entirely.

def select_model(tracker, primary_model, fallback_model):
    if tracker.spent_usd >= tracker.max_usd * 0.6:
        return fallback_model
    return primary_model

This works well for tasks with diminishing returns on model capability as the task progresses, summarization passes, final formatting steps, or straightforward tool-result relaying, where a smaller model does the job just as well once the hard reasoning is already done. It works poorly for tasks where the last step is the hardest one, like a final correctness check on generated code, so decide per agent type whether tiering helps or just moves the failure to a place you'll notice later.

Reporting: Make Cost Visible Before It's a Surprise

Budgeting stops runaway tasks. Reporting stops runaway patterns across many tasks. Log at minimum, per agent run: task ID, model used, total tokens (broken into input, output, cache read, cache write), total cost, number of steps, and final status (complete, budget_exceeded, max_steps_reached). Store it somewhere queryable, even a simple structured log file works, so you can answer "which agent type is our biggest spender this week" without re-deriving it from raw API logs.

import json
import time

def log_task_cost(task_id, agent_type, tracker, status, steps):
    record = {
        "task_id": task_id,
        "agent_type": agent_type,
        "spent_usd": round(tracker.spent_usd, 6),
        "calls": tracker.calls,
        "steps": steps,
        "status": status,
        "timestamp": time.time(),
    }
    with open("agent_cost_log.jsonl", "a") as f:
        f.write(json.dumps(record) + "\n")

A weekly rollup of this log tells you two things budgets alone can't: which agent types are trending toward their ceilings (a sign your budget is too tight or the task is genuinely getting harder), and which agent types never come close (a sign you're over-provisioning and could tighten the budget without any quality loss).

Common Agent Cost Budgeting Mistakes

  • Setting one global budget for every task type. A support-ticket triage agent and a codebase-migration agent have wildly different legitimate costs. One number for both either throttles the expensive one or wastes money on the cheap one at scale.
  • Budgeting only the primary model, not sub-agents. If your orchestrator spawns sub-agents, each one needs its own tracker instance, and the parent needs to sum them, or fan-out costs slip through uncounted.
  • Checking the budget only at the start of a task. A budget check before the loop starts catches nothing; you have to check after every single model call, inside the loop, or a single long-running task blows past the ceiling before anyone notices.
  • Discarding partial work on budget exceeded. Returning nothing when a task hits its ceiling wastes the money you already spent getting partway there. Return the partial transcript or result.
  • Ignoring cache token fields in cost math. Treating cached input tokens at full input price makes your budgets look tighter than they actually are and can trigger unnecessary downgrades or stops.
  • No per-session ceiling. A per-task budget alone doesn't stop a user from running the same task fifty times in a row. Add a session-level or user-level rolling limit alongside the per-task one.

FAQ

What's a reasonable starting budget for a single agent task? There's no universal number since it depends entirely on model choice, task complexity, and how many tool calls the task typically needs. Start by running the agent manually on ten to twenty representative tasks with tracking on but no enforcement, look at the actual distribution of costs, and set your hard ceiling at roughly two to three times the median observed cost. That gives normal tasks headroom while still catching genuine runaways.

Should budgets be enforced client-side or server-side? Server-side, if your agent runs anywhere a user could tamper with client code. A budget check in browser JavaScript is a suggestion, not an enforcement mechanism. The tracker and circuit breaker belong in the same backend process that's making the actual API calls.

Does agent cost budgeting slow down the agent? The tracking itself adds negligible overhead, it's just arithmetic on a field the API response already includes. Any slowdown comes from added latency if you route through a separate budget-check service instead of an in-process object, so keep the tracker in the same process as the loop when task latency matters.

How do sub-agents fit into a single task's budget? Pass the parent tracker's remaining budget down to each sub-agent, or give sub-agents their own smaller trackers and have the orchestrator sum them against its own ceiling after each sub-agent completes. Either way, the parent task's total cost must include every sub-agent's spend, not just the orchestrator's own model calls.

Is prompt caching enough to solve cost problems on its own? No. Caching reduces the cost of repeated, stable context, but it does nothing about genuinely new work: growing tool outputs, retry loops, or sub-agent fan-out. Treat caching as one lever among several, not a substitute for an actual budget ceiling and circuit breaker.

What should happen when a task hits max_steps but not the cost budget? Treat it the same as a budget-exceeded event for reporting purposes, since it usually signals the agent is stuck in a loop that hasn't yet become expensive but will. Return the partial transcript, log it, and consider whether the task needs a different tool set or a clearer termination condition rather than just raising the step limit.