DEV Community

Zira
Zira

Posted on

Your AI Agent Needs a Cost Circuit Breaker, Not a Monthly Budget

A monthly spend limit is useful for accounting. It is a poor safety control for an AI agent.

An agent can burn through a budget long before the invoice arrives: a retry loop, a tool that returns oversized context, a fallback model that is more expensive than the primary model, or several workers all repeating the same request. By the time someone notices the dashboard, the expensive behavior has already happened.

The control I want instead is a cost circuit breaker: a small state machine that reserves estimated spend before work starts, records actual usage, and changes what the agent is allowed to do as the budget is consumed.

This is not a claim that a circuit breaker makes model usage cheap. It makes overspend a bounded and observable failure mode.

1. Separate four kinds of budget

Do not put every limit into one 'max_tokens' field. Track at least these dimensions:

  • Run budget: the maximum estimated cost for one user request.
  • Tenant or project budget: the amount that a workload may reserve in a time window.
  • Retry budget: the number and cost of retries allowed for one operation.
  • Provider budget: a cap for each model/provider so fallback cannot silently consume the whole allowance.

A request can be under its token limit and still violate a provider or retry budget. The decision should be made against all four.

A minimal reservation record can look like this:

type BudgetReservation struct {
    RunID          string
    OperationID    string
    Provider       string
    EstimatedCents int64
    ActualCents    int64
    RetryIndex     int
    State          string // RESERVED, SETTLED, RELEASED, UNKNOWN
}
Enter fullscreen mode Exit fullscreen mode

The important part is not the language. It is that the reservation has a stable OperationID and a durable state.

2. Reserve before dispatch, settle after the response

The dangerous order is:

  1. Call the model.
  2. Read the usage fields.
  3. Decide whether the call was affordable.

That order has already spent the money. Instead:

  1. Estimate the worst-case input and output cost.
  2. Atomically reserve that amount against the applicable budgets.
  3. Dispatch the request with the reservation ID.
  4. Settle the reservation using provider usage data.
  5. Release unused capacity, or mark the reservation UNKNOWN if the outcome is ambiguous.

The reserve operation needs to be atomic. Two workers must not both observe the same remaining balance and both receive permission.

A simple SQL shape is enough to make the invariant explicit:

UPDATE budget_windows
SET reserved_cents = reserved_cents + :estimate
WHERE budget_key = :key
  AND reserved_cents + settled_cents + :estimate <= limit_cents;
Enter fullscreen mode Exit fullscreen mode

If the affected-row count is zero, reject or downgrade the operation. Do not enqueue it and hope a later worker notices.

3. Make the breaker change behavior

A useful breaker has more than open and closed states. For example:

  • NORMAL: full tool and model policy.
  • THROTTLED: lower concurrency and shorter output ceilings.
  • DEGRADED: allow read-only tools and a cheaper approved model.
  • OPEN: reject new work, but allow reconciliation of in-flight requests.
  • UNKNOWN: stop automatic retries until provider usage and delivery state are reconciled.

The transition should be based on reserved plus settled spend, not only settled spend. Otherwise a burst of concurrent requests can oversubscribe the remaining budget.

Example policy:

Remaining allowance Policy
More than 40% Normal execution
15% to 40% Reduce concurrency and cap output
1% to 15% Read-only or low-cost model only
0% or negative Open for new work

The percentages are examples, not universal defaults. Tune them from your workload and record why a transition occurred.

4. Treat retries as new reservations

A retry is not free just because the original request failed from the application’s point of view. The provider may have processed it, and a timeout may leave the outcome unknown.

Give every attempt a stable operation identity plus an attempt number:

run_482 / summarize_invoice / attempt_2
Enter fullscreen mode Exit fullscreen mode

Before retrying:

  • look up the provider request ID if one exists;
  • reconcile usage for the previous attempt;
  • check whether the tool side effect completed;
  • reserve the retry cost separately;
  • stop when the retry budget is exhausted.

This is especially important for always-on agents. A restart loop can turn one logical task into hundreds of billable attempts. If you run OpenClaw or another agent continuously, managed OpenClaw hosting on Ampere can be one hosting option to evaluate for the runtime, but hosting does not remove model-cost, retry, or credential risk. The breaker still belongs in the agent control plane.

5. Do not let fallback bypass the breaker

Fallback logic often has an accidental escape hatch:

def call_with_fallback(request):
    try:
        return call('cheap-model', request)
    except TimeoutError:
        return call('premium-model', request)
Enter fullscreen mode Exit fullscreen mode

Both calls need the same budget checks. Better:

def dispatch(request, provider):
    estimate = price(provider, request.input_tokens, request.max_output_tokens)
    reservation = reserve_all(provider, estimate, request.run_id)
    if not reservation:
        raise BudgetOpen('no capacity for this provider')
    try:
        response = call(provider, request, reservation.id)
        settle(reservation.id, usage=response.usage)
        return response
    except TimeoutError:
        mark_unknown(reservation.id)
        raise
Enter fullscreen mode Exit fullscreen mode

Fallback then becomes a policy decision made after reconciliation, not an exception handler that can spend outside the guardrail.

6. Test the failure paths deliberately

A cost control that only works on successful responses is not a control. Inject these cases in staging:

  • two workers reserve the final available cents concurrently;
  • the provider times out after accepting the request;
  • usage data arrives late or is missing;
  • a retry is attempted after the breaker opens;
  • a premium fallback is requested while the provider budget is exhausted;
  • a process crashes after reservation but before settlement;
  • a process crashes after settlement but before releasing the unused estimate;
  • the clock moves across a budget-window boundary;
  • a duplicate message is delivered to two workers.

For every case, assert an invariant: the ledger is reconcilable, no reservation is silently lost, no automatic retry bypasses the breaker, and the final charge is attributable to one run and attempt.

7. Measure the control, not just the invoice

Track:

  • estimated versus actual cost by provider;
  • reserved capacity that remains UNKNOWN;
  • retry cost per operation;
  • fallback rate and fallback cost;
  • time spent in each breaker state;
  • rejected work and its reason;
  • maximum concurrent reserved spend.

The useful question is not only “How much did this month cost?” It is “Which state transition prevented the next unbounded retry or fallback?”

A monthly budget tells finance what happened. A cost circuit breaker gives the runtime a chance to stop, degrade, or reconcile before a small failure becomes a large bill.

If you build agents, follow for practical control-plane patterns around state, permissions, recovery, and deployment rather than model demos alone.

Top comments (0)