A daily AI budget that fails closed: caps, session ids and a gap we found
One unattended job once made about 118,000 model calls in a day for under eighteen dollars, and we could not tell which job it was. Two rules would have stopped it: a daily cap that refuses when unset, and a session id on every call.
π― For A developer who runs AI calls in loops, cron jobs or background workers
- What a runaway AI job looks like in your own numbers
- Why a cap must refuse when it is missing, and why every call needs a label
- How to check your own budget helper for the gap we found in ours
π₯ What happened
On 26 June 2026 our trace database recorded 117,917 model calls for one day at a cost of $17.88. The day before had 1,809 calls and $2.14. That is about 65 times the volume. Of the 117,917 calls, 116,382 carried no session id, so the trace could not say which job made them. At first the blame went to the wrong job. Only the per-day, per-session numbers pointed to the real one, an automatic translation loop. Our incident note puts the damage at roughly seventeen and a half dollars. The trace for that day shows $17.88, which also includes the ordinary traffic of the day.
The money was small. The lesson was not: a loop that calls a model for every item, with no cap, scales to whatever the input has.
π§± Two rules
Rule one: every automatic path has a daily cap, and a missing cap means refuse. The cap is read from an explicit setting. If the setting is empty, zero or negative, the job must not start. The opposite default, "no setting, no limit", turns a forgotten line of configuration into unlimited spending.
Rule two: every call carries a session id. The id names the job, not the person. Without it, the first question after an alarm, "who did this?", has no answer. We do not commit automation that calls a model without one.
π The gap we found while writing this
Our helper that tracks a daily budget for background jobs reads the limit from an environment variable. We ran it on 3 November 2026 with the variable empty. The limit came out as 0.0 and the call to ask whether spending is allowed returned yes. In other words, in that helper a missing cap means no cap, which is the opposite of rule one. Callers that set a positive limit are fine. A caller that forgets the variable is not protected.
Our metered transport, the lane that pays providers, behaves correctly: it refuses with "explicit daily cap required" when the cap is missing. The two helpers disagree, and we have recorded the helper as a defect to fix. The lesson is to test your own guard with the setting removed.
π How big is the unlabelled share
Since 2026-04-24 our trace database holds 777,526 calls costing $164.31 in total. Of these, 125,541 calls, 16.1 percent, have no session id, and they cost $24.57, 15.0 percent of the total. Most of that comes from the one day above. Unlabelled spending is not a rounding error.
π οΈ Practice: close both gaps
- List every place that calls a model in a loop, on a schedule or in the background.
- For each, find the daily cap setting. If there is none, add one. If the code treats empty as unlimited, change it to refuse.
- Write a test that starts the job with the setting removed and asserts that no call is made.
- Add a session id to every call. Make the wrapper raise an error when it is missing.
- Query your traces for calls with no id, per day. Make that query an alarm.
- Set the alarm threshold from your normal day, for example three times the median of the last fourteen days.
β Check yourself
- Unset the cap for one job in a test environment. The job refuses and says why.
- Pick any call from last week. You can name the job that made it.
- The count of calls without an id is zero for the last seven days, or you can explain each one.
- You can read yesterday's spend per job from one query.
π§ When this does NOT work
- A cap that is too high. A cap of a hundred dollars protects nothing on a ten-dollar job. Set it from the real need.
- Spend that arrives late. Some providers report cost after the call. A cap based on reported spend can be overshot by what is in flight. Reserve before you call.
- Shared keys. If five jobs share one key, the provider's bill cannot separate them. Your session id has to.
- Retries. A retry loop can multiply spend. Count every attempt against the cap.
- Free models. Free tiers have rate limits and may disappear. A cap of zero dollars still needs a call limit.
π Evidence
| What we measured | Calls and cost per day, and calls without a session id, in our trace database; behaviour of the budget helper with an empty limit |
| When | 3 November 2026, 22:06 UTC |
| Artifact | The LLM trace database (777,526 calls since 2026-04-24); the day of 26 June 2026 and a direct run of the helper |
| Limit | One incident and one helper. The trace shows volume and cost, not which job it was; the job is named in our incident note |
β‘οΈ Next
Write the limits down with Costs, budgets and limits, and decide what happens when a job goes wrong in An AI incident playbook for a small team.
Sources
- Costs, budgets and limits: set a budget per task.
- An AI incident playbook for a small team: response steps.
- Context windows and tokens: what an agent can see and what it costs.
- Models and routing: what routing changes.
π Article connections
Open the full knowledge graph βPublished article links. Select a node to open its article at the heading.
Article links Β· keyboard / text view
Same topic
- A vector base is a map, not a source: what we learned from retrieval
- When you cannot tell what a model call cost: freeze, then reconcile
- Splitting a big task for a cheap model: the 24,000-byte lesson
- Skills, MCP tools and rules: three ways to give an agent capabilities
- Eight shell and git mistakes our agents made, and the rule for each
- Several agents, one repository: ownership rules from real incidents
Next steps
Related reading
A vector base is a map, not a source: what we learned from retrieval
A vector base finds passages that are close in meaning to your question. It does not know which are true or current. Treat every hit as a pointer: open the file it names, check the hash, and take numbers from the artifact, never from the retrieved paragraph.
AI agents and MCP explainedWhen you cannot tell what a model call cost: freeze, then reconcile
Sometimes a model call times out and you do not know whether the provider charged you. Never record that as zero. Mark it uncertain, stop new paid calls, and let a named person reconcile it with the provider's statement. Refusals before generation are the exception.
AI agents and MCP explainedSplitting a big task for a cheap model: the 24,000-byte lesson
A cheap model does well on one small, well-defined job and badly on a big pile. We cap a task at 24,000 bytes and refuse larger ones before any model is called. Split by unit, one file or one country per task, and check the size before you start.
AI agents and MCP explainedSkills, MCP tools and rules: three ways to give an agent capabilities
Agents get abilities in different ways. A rule constrains what they do, a skill tells them how to do a job, and an MCP tool lets them call a function. Each has its own risk, so declare all three in a register and test that nothing exists unlisted.
π¬ Comments