A budget check that runs inside the job it is supposed to protect cannot protect anything. If the cost estimate throws an exception, times out, or returns null, a naive if cost > limit: raise is skipped and the job proceeds with the full spend. Fail-closed means the default answer is "no": the gate is a separate executable, the pipeline calls it first, and anything other than an explicit pass with a recorded approval (or a recorded pass) blocks the run.
The cost is real and lumpy. Agentic multi-step tasks commonly consume 15,000 to 1,000,000 or more tokens per request, against 200 to 2,000 for a simple chatbot reply, according to the Iternal token usage reference. A runaway loop is not a wrong answer, it is a bill, as TrueFoundry's write-up on rate limiting agents puts it. That framing matters: the gate exists to bound blast radius, not to be clever.
The gates are the same shape as error budget gates in SRE practice. An error budget policy defines what happens when a budget is exhausted: releases halt until the service is back within its objective, with exceptions and an escalation path. Google's example error budget policy is explicit that if the service has exceeded its budget over the preceding four-week window, changes halt except P0 and security fixes. Your cost budget should read the same way. The difference is that cost limits need a hard refusal at the start of the run, because a job that starts cannot un-spend.
One design decision up front: what counts as "observed spend." There are three points worth logging separately, and conflating them is the most common source of arguments later.
| Measurement point | What it tells you | Failure mode if used alone |
|---|---|---|
| Pre-flight estimate | What this run is about to cost | Underestimates code, JSON and non-English text |
| Month-to-date actuals | What the budget has left | Stale by minutes, races with parallel jobs |
| Post-run usage | What it actually cost | Too late to block anything |
Use the pre-flight estimate for the decision, month-to-date actuals for the limit comparison, and post-run usage for reconciliation. Log all three.



