Spending limits: what your Bun backend needs to have
You push a Bun script on a Friday night. It uses an agent that calls an LLM API in a loop. You go to bed. On Saturday morning, the billing email shows up before your coffee. This is the nightmare Simon Willison decided to face head on. In his post We're going to need default hard budget caps on pretty much everything, he argues that a hard spending limit should be on by default in almost every paid service. In this article I explain the idea, why it matters more now, and how to apply it in your Bun code today, without waiting for anyone.
What Simon Willison is arguing for
The argument is simple. Today, most services charge by usage and have no cap. You set an alert, get an email when you cross a certain amount, and the service keeps running. And charging.
The proposal is to flip the default. Every new account would start with a hard cap. Hit the cap, and the service stops. If you want to spend more, you raise the limit on purpose.
This always made sense for the cloud. But back then the risk was a public bucket or a misconfigured function. Now there's something new in the room: code that decides on its own how many times it will call a paid API.
Why a spending alert is not a spending limit
A lot of people think they're already protected because they set up an alert. They're not.
An alert is a notification. It lands in your inbox, goes to spam, or arrives at 3 a.m. while you sleep. The loop doesn't sleep. It keeps going.
An alert tells you the money is gone. A limit stops it from leaving.
Run the numbers with a coding agent. Say each call sends 200k tokens of context, at $3 per million input tokens. That's $0.60 per call, not counting output. If the agent gets stuck in a "try to fix the test, fail, try again" cycle and runs 1,000 times overnight, that's $600. For a lot of people, that's months of server costs thrown away because of a flaky test.
And 1,000 iterations isn't even a lot. A fast agent does that in a few hours.
I feel the upside of a cap firsthand. My Vercel account is on the free plan and has a limit of 100 deploys per day. Has it annoyed me? Sure. But I've never gotten a surprise bill. A hard limit is a pain right up until the day it saves you.
Where Bun fits into this
Bun made it very easy to do things that cost money. That's a compliment, but it comes with a price.
bun run agent.tsruns TypeScript directly, no build step. An automation script is born in two minutes.Bun.servespins up an HTTP endpoint in five lines. Exposing a route that calls an LLM became trivial.- The runtime is fast. A loop that calls
fetchfinishes sooner, which means it also spends faster.
None of this is Bun's fault. The problem is that the friction is gone. It used to take enough setup to build a service that called a paid API that you'd stop and think about cost. Today you don't stop. You just run it.
So until providers adopt the default Simon proposes, the responsibility falls on whoever writes the code. The good news: Bun already ships with everything you need for this, nothing to install.
How to add a spending limit to your Bun code
The idea is to have a persistent counter that runs before every paid call. If the estimated spend goes over the daily cap, the code throws an error and stops. I use bun:sqlite, which is built into the runtime:
// budget.ts
import { Database } from "bun:sqlite";
const db = new Database("budget.sqlite");
db.run(`CREATE TABLE IF NOT EXISTS spend (
day TEXT PRIMARY KEY,
cents INTEGER NOT NULL
)`);
const DAILY_LIMIT = Number(process.env.DAILY_LIMIT_CENTS ?? 500); // $5
export function reserve(cents: number) {
const day = new Date().toISOString().slice(0, 10);
const { total } = db
.query(`INSERT INTO spend (day, cents) VALUES (?1, ?2)
ON CONFLICT(day) DO UPDATE SET cents = cents + ?2
RETURNING cents AS total`)
.get(day, cents) as { total: number };
if (total > DAILY_LIMIT) {
throw new Error(`Daily spending limit exceeded: ${total} cents`);
}
}
Notice one detail: if the limit is exceeded, the amount has already been added. That's on purpose. The system fails closed. Once it hits the cap, every call after that also fails until the day rolls over or you change the limit.
Now the usage, with two extra safeguards that cost one line each:
import { reserve } from "./budget";
const MAX_STEPS = 25;
for (let step = 0; step < MAX_STEPS; step++) {
const tokens = estimateTokens(context);
reserve(Math.ceil((tokens / 1_000_000) * 300)); // $3/M in cents
const res = await fetch(API_URL, {
method: "POST",
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30_000),
});
if (await isDone(res)) break;
}
There are three layers:
- A money cap per day, which survives restarts because it lives in SQLite.
- A step cap on the loop. An agent that needs more than 25 steps is probably going in circles.
- A timeout on every request, so no call hangs forever.
The token estimate doesn't need to be perfect. A rough "characters divided by 4" rule is good enough for a cap. The goal isn't accounting. It's having a handbrake.
If your service runs on more than one instance, local SQLite can no longer be the single source of truth. Then the same INSERT ... ON CONFLICT moves to PostgreSQL, which you probably already have. The logic stays the same.
What to configure outside the code
Your code is just one of the layers. A bug in it, or a leaked API key, bypasses everything. So it's worth closing the doors from the outside too:
- A limit at the LLM provider. Most consoles let you set a monthly cap per project or workspace. Create a separate key for each app, with its own limit. An experiment key should never have the same cap as a production one.
- A cloud budget with an action, not just an email. If the platform lets you pause the project when it hits the amount, turn it on. A site being down for a few hours costs less than the bill.
- Rate limiting on the public route. If you exposed a
Bun.servethat calls an LLM, a bot making 50 requests per second turns your daily cap into a five-minute cap. Good thing there's a cap, but better not to hit it at all.
None of these take much work. The hard part is remembering to do them before the incident, not after.
Will providers adopt this by default?
Here's my honest take: not anytime soon, and not willingly.
Uncapped billing is a comfortable business model. Accidental spending is still revenue. Some providers refund the money when a customer complains on Twitter, but refunding later is very different from preventing it up front. People without an audience to complain to publicly usually just pay and stay quiet.
What could change the game is volume. When millions of devs are running agents in loops, surprise bills stop being isolated cases and turn into support tickets, chargebacks and lost customers. That's when the default changes, purely because of cost.
Until then, treat every paid service as if it has no cap, because it probably doesn't. Add a spending limit to your Bun code today. It's about 20 lines, and they're worth more than any pretty alert on a dashboard.
It's the kind of detail I put into every project that touches AI. If you want to see how this looks in a real app, take a look at the projects I've built.
LinkedIn summary
A spending alert protects no one. It just tells you the money is already gone. Picture an AI agent running in a loop on a Friday night. That's 1,000 calls at $0.60 each while you sleep, and on Saturday the bill arrives before your coffee. Simon Willison argues that every paid service should ship with a hard cap turned on by default. I agree, but I'm not waiting for providers to change. In my Bun backend I solve this with about 20 lines: a daily counter in bun:sqlite, a step limit on the loop and a timeout on every request. A hard limit is annoying day to day, and it saves you on exactly the day something goes wrong. I wrote up the step by step with the full code on the blog. If you run AI in production, it's worth the 5 minutes. #Bun #TypeScript #AI #LLM #Backend