An autonomous agent on a retry loop can spend without bound. It never errors. It just keeps calling the model. Amazon ran an internal Claude deployment 860% over budget for five months on a misconfigured retry loop before anyone noticed, a bill reported near $1.8M. The failure is not the model. It is the missing floor under the loop. We built the floor.
What we built
fleet_guard is a small, zero-dependency budget guard for any
loop that calls a paid model. You give it a cap, tokens, cost in dollars,
iterations, or a combination, and call tick() once per
iteration. When a cap is crossed it raises BudgetExceeded,
which is not a subclass of any provider error, so it propagates cleanly out
of a loop that only catches transport errors and actually halts it.
guard = BudgetGuard("nightly-summariser",
max_cost_usd=5.0, max_iterations=500, window_s=3600)
while work_remaining():
guard.tick(tokens=resp_tokens, cost_usd=resp_cost)
do_one_iteration()
Why in-memory counters are not enough
Most guards keep the count in a variable. That fails against the exact shape of loop that causes the damage: one that restarts the process each iteration. A cron every minute, or a supervisor that respawns the agent on crash, resets an in-memory counter every time, so the cap never bites. Our own router already had a per-session token cap, and it had this weakness: in-memory, single-provider, gone on restart.
fleet_guard persists the count to disk and measures spend over
a rolling time window. The floor holds across restarts, and old spend
decays on its own, so max_cost_usd=5, window_s=3600 reads as
"no more than $5 in any rolling hour" and survives the process dying and
coming back. One JSON file per guard, written atomically before the guard
raises, so the ledger reflects the spend that tripped the cap.
The demo
demo.py reproduces the failure with no network and no keys.
The runaway loop, capped only so the demo stays finite, runs 100,000 calls
and spends $200 and is still going. Then the same loop with one line added:
halted after 500 calls, $1.00 spent.
reason: cost $1.0020 > cap $1.0000
Then it starts a fresh guard on the same ledger, the restart case, and that one is over cap on construction: it never runs a single call.
The kill-switch
One file halts every guard sharing a ledger directory, with no deploy and
no code change. python3 fleet_guard.py stop drops it,
resume clears it. It is the thing you reach for at 2am when an
agent is misbehaving and you want everything to stop now, not after a push.
Why it was worth doing
Runaway spend is not a tail risk for people running autonomous agents, it is the default outcome of a loop with a bad exit condition and no floor. The cost of the guard is one function call per iteration. The cost of not having it is measured in months and, in the worst reported case, seven figures.
What's still off
The guard counts what you pass it. If your tick() under-reports
cost, the floor sits higher than you think, so the honest version pulls
cost from the provider's own usage field, not an estimate. It also guards a
single loop's ledger, not a true fleet-wide total across every agent at
once; that needs a shared ledger and a lock, which is the next iteration.
For a per-agent floor that holds across restarts, this is live and it stops
the bleed.