How it worksLive demoBlogFAQPricingGitHub
Blog / EngineeringOctober 12, 2026
Per-customer AI budgets

How to stop one customer from blowing up your OpenAI bill

If you run a SaaS on top of an LLM API, your provider bill is one number. OpenAI and Anthropic let you set limits per organization or project. Those limits protect your account, not your margins: they can't tell you which customer drove the spend, and they can't stop one customer without stopping everyone.

There are two ways this goes wrong:

  1. The runaway user. Someone scripts your feature, or a client bug retries in a loop, and one account burns real money overnight.
  2. The quietly unprofitable account. A customer on a $20 plan sends long contexts every day and costs you more in tokens than they pay. You can't see it because cost isn't attributed per customer.

Here's a pattern that fixes both, and the edge cases to watch for.

1. Price every call in dollars, per customer

Every chat completion returns token usage. Convert it to dollars immediately, because token counts aren't comparable across models:

cost = (uncachedInput * inputPrice + cachedInput * cachedInputPrice + output * outputPrice) / 1_000_000
  • Cached input tokens: OpenAI reports them in usage.prompt_tokens_details.cached_tokens, and they're billed at a lower rate. Ignore them and you'll over-count.
  • Dated snapshots: model names like gpt-5.4-mini-2026-03-17 should resolve to the base model's price.
  • Price table: keep it in one place, with a source link and the date you read it. Prices change. When I checked OpenAI's pricing page on 2026-10-09, it listed gpt-5.5 at $5 input / $30 output per 1M tokens and gpt-5.4-mini at $0.75 / $4.50. Verify before you rely on that.

2. Check before the call, record after it

before call:  spent = store.get(customer, month)
              if spent >= budget        → block
              if spent >= 0.8 * budget  → swap to a cheaper model
call provider
after call:   store.add(customer, month, cost)

The check has to sit in the request path, before the call. A nightly job that emails you the damage is forensics, not protection.

3. Handle streaming properly

With stream: true, OpenAI only reports usage if you set stream_options: { include_usage: true }, and it arrives in the final chunk. Wrap the async iterator, pass chunks through unchanged, and record the cost in a finally block so it still runs when the client stops reading early.

4. Degrade before you block

A hard cutoff is a bad experience for a paying customer. A softer first step works better: past 80% of the budget, route the same task to a cheaper model. Many users won't notice, and your margin is protected. At 100%, block with a specific error so your UI can say "You've reached this month's AI limit. Upgrade?" instead of showing a generic failure.

5. Use a store that handles concurrency

In-memory counters are fine for one process. With several instances or serverless functions, use an atomic increment such as Redis INCRBYFLOAT llmbrake:{month}:{customer} {cost}. Key it by month and budgets reset without a cron job; add an EXPIRE so old months clean themselves up.

Two honest caveats:

  • Overshoot: concurrent in-flight requests can each pass the check and then overshoot together, usually by the cost of those few calls. If you need a strict ceiling, reserve an estimated cost before the call and settle afterwards.
  • Store outages: decide whether to fail open (keep serving and risk some unmetered spend) or fail closed (protect spend and break the feature). For most products, failing open plus an alert is the right call.

6. Compare cost to revenue

Once spend is attributed per customer, join it with what they pay you, for example Stripe subscriptions. A table of customer, AI cost, revenue and margin is usually enough to spot plans that need repricing or a lower cap.

The library version

I packaged this pattern as LLMBrake, a small MIT-licensed TypeScript library (about 300 lines, zero runtime dependencies):

npm install llmbrake
import { createGuard, isBudgetExceededError } from "llmbrake";

const guard = createGuard({
  budgetUsd: (ctx) => (ctx.plan === "pro" ? 20 : 2),
  downgradeAt: 0.8,
  downgradeMap: { "gpt-5.5": "gpt-5.4-mini" },
});

const chat = openai.chat.completions;
const create = guard.wrap(chat.create.bind(chat));

const res = await create(
  { model: "gpt-5.5", messages: [{ role: "user", content: "Hello" }] },
  { customerId: user.id, plan: user.plan, feature: "chat" },
);
// at the cap → throws BudgetExceededError

It covers cached-token pricing, streaming usage, the downgrade step, a typed block error, metadata-only usage events, and a pluggable store with a fail-open default. Use it, or copy the pattern.

Disclosure: I build LLMBrake. I'm also gauging interest in a hosted dashboard (shared counters, alerts, AI cost vs. revenue per customer). It doesn't exist yet; there's a free waitlist at llmbrake.com.


Try the live demo (a browser simulation, no API keys needed) or read the README.