Navigation
What We AreThe BrainPortfolioThe Lab's LabBuilt For YouThe WhiteboardServices & Prices
Let's Talk →
← The Whiteboard

How do you cap OpenAI spend per customer?

The budget setting is an email. The only native hard stop kills your whole organisation. The cost API buckets by the day. Four controls that hold, and the counter that enforces them.

Cap OpenAI spend per customer in your own gateway. No first-party provider offers a synchronous per-end-customer hard cap: OpenAI's monthly budget sends email, the only native hard stop is org-wide credit exhaustion, and the cost API buckets by the day. Enforce four controls before each call — per-run ceiling, per-customer window budget, model-tier routing, velocity circuit breaker.

Ross Jones — Founder, The Hopium Lab. Last modified 22 July 2026. Twenty-four shipped products across fifteen sectors.

Why doesn't OpenAI's budget limit stop the spend?

Because the setting is a notification, not a cut-off. Customers noticed on 2 April 2025, reporting that the option had become "an Alert, rather than a Hard Cut Off". Several pages currently ranking for this question date the change to early 2026. That dates the change no later than April 2025, and it is checkable in one search.

One native hard stop does survive. billing_hard_limit_reached fires when prepaid credits run out with auto-recharge off — an organisation-wide kill switch that stops every paying customer in order to stop one abusive one, with reported lag before the cutoff lands.

The account-level ceiling is real and made of published numbers. OpenAI's usage tiers run Free and Tier 1 at $100 a month, Tier 2 $500, Tier 3 $1,000, Tier 4 $5,000, Tier 5 $200,000. A Tier 5 organisation can spend two hundred thousand dollars in a month before anything provider-side intervenes.

Which spend controls actually fire before the call?

Rate limits do. Nothing the model providers denominate in money does. OpenAI's documentation states that rate limits are defined at organisation and project level, not user level.

ControlScopeFires before the call?What it actually does
OpenAI monthly budgetOrganisationNoEmails and banners. Requests continue.
Prepaid credit exhaustionOrganisationEventuallybilling_hard_limit_reached — stops everyone at once
OpenAI RPM/TPM limitsOrg and projectYesCaps volume, not money; 429 plus x-ratelimit-remaining-* headers
OpenAI costs endpointOrg, project, keyNobucket_width supports 1d and nothing finer
Anthropic Spend Limits APIHuman seat membersYes, per seatGenuine per-user monthly caps — Enterprise organisations only, not Console
Anthropic usage/cost APIOrg, workspace, keyNo~5-minute lag; cost report daily-only; recent days provisional
Azure OpenAISubscriptionNoMicrosoft's answer: no supported hard cap beyond TPM/RPM quotas
OpenRouter key limitOne key per end-userYes402 on exhaustion; daily/weekly/monthly reset at midnight UTC
LiteLLM customer budgetThe user paramYes, if fail-closed401 ExceededTokenBudget; Redis hot path, database reconciled behind it

Three rows carry caveats. Anthropic's Spend Limits API sets real per-user caps with an approve/deny queue — for human seats inside a Claude Enterprise organisation, not the end-customers of an API-backed product, and the docs call the spend reading informational rather than transactional. OpenRouter enforces the synchronous per-key ceiling the first parties refuse to, though every key draws on one shared balance. Microsoft's advice for Azure is to build the logic into your own application. Aggregators and gateways ship the synchronous per-key cap. Model providers did not.

The billing API cannot power the cap. OpenAI's costs endpoint buckets at one day, Anthropic's cost report is daily-only, and Anthropic documents its per-member spend reading as informational rather than transactional. A daily-granular, explicitly non-transactional data source cannot enforce a synchronous ceiling. Granularity is the argument; latency only decides how long you bleed before you find out.

What does five minutes of a runaway loop cost?

Three hundred and nine dollars, one agent, list price. Prices retrieved 22 July 2026: GPT-5.6 Sol is $5.00 per million input tokens and $30.00 per million output, before the 10% surcharge that models released after 5 March 2026 carry on data-residency regional endpoints. A loop carrying a 200,000-token context and returning 1,000 tokens costs $1.00 in and $0.03 out — $1.03 a call. One agent looping once a second through the five minutes Anthropic documents as its typical reporting lag, the fastest first-party figure either provider publishes, is 300 calls and $309. Ten concurrent runs: $3,090.

OpenAI publishes no such window. Its costs endpoint buckets by the day, so the same loop runs unseen for up to 24 hours and $89,000 — less whatever your TPM ceiling throttles away, which is a volume limit doing a money limit's job badly. Left running, one loop bills $3,708 an hour and walks a Tier 5 organisation into its $200,000 ceiling in roughly 54 hours. Ten concurrent loops reach the same $200,000 ceiling in under six hours.

Cache the context at $0.50 per million and the call drops to $0.13 — $39 for the same five minutes. Caching is a discount, not a ceiling. Route the same call shape to GPT-5.4-nano at $0.20 and $1.25 and 300 calls cost $12.38: a 25x spread on input and 24x on output between the top of the 5.6 line and the bottom of the 5.4 line, which is the case for tier routing in one number.

In May 2026 the creator of OpenClaw published a screenshot of $1,305,088.81 of OpenAI API spend in thirty days — 603 billion tokens, 7.6 million requests, around a hundred Codex agents. Three caveats, all load-bearing: he works at OpenAI, OpenAI paid the bill, and most of the figure reflects Fast Mode pricing, which he put nearer $300,000 without it. A third of a million dollars a month of API spend from a hundred agents, deliberately run, is the sober number. Nobody in that story was trying to lose money.

Why can't the agent enforce its own budget?

Because an agent asked not to spend money can be talked into spending money. NIST's AI safety team (now CAISI), with the UK AI Security Institute, measured agent hijacking against a Claude 3.5 Sonnet deployment on AgentDojo on 17 January 2025: 11% success under the strongest baseline attack, 81% under the strongest adaptive one. One model, one harness, eighteen months ago — and I have seen no published result since that closes the adaptive-attack gap. The taxonomy that named the attack class, NIST AI 100-2 E2025, has been final since 24 March 2025.

OWASP has the name for the resulting bill: LLM10:2025 Unbounded Consumption, which includes Denial of Wallet — attackers exploiting per-use pricing until the financial burden becomes unsustainable. Prompt injection is LLM01:2025, excessive agency LLM06:2025. Use the current identifiers; the 2023 list's Model Denial of Service was renamed and renumbered.

A budget written into a system prompt is a request, not a control. When NIST red-teamed one agent adaptively on AgentDojo, the hijack rate went from 11% to 81%.

Enforcement belongs in code the model cannot read, cannot reason about and cannot reach. Prompt-as-policy fails for the same reason everywhere else: the harness enforces, the model may only ask.

What are the four controls that hold?

A per-run ceiling, a per-customer window budget, model-tier routing and a velocity circuit breaker. Each catches a failure the other three miss.

Per-run ceiling. A cumulative cost accumulator across every call in one agent run, checked before each call. max_output_tokens is not a per-run ceiling: it bounds one generation's output, not input tokens, not the number of turns, not tool fan-out. On a 200,000-token context returning 1,000 tokens, input is 97% of the bill. And reasoning tokens are billed as output and counted inside the limit — several third-party docs claim they escape it, which prices every worst case too low.

Per-customer window budget. A spend total over a window that starts on that customer's first spend, keyed on your customer identifier, never on the calendar. Anchored to first spend, it still tumbles — it just tumbles on a schedule an abuser cannot predict. Calendar resets hand an abuser a fresh allowance at midnight on the first.

Model-tier routing. Degrade before you deny. Serve the first N cents on the strong model, the remainder on a cheap one, refusal only after both.

Velocity circuit breaker. A breaker that trips on the rate of spend rather than the total: cost per fixed time bucket, refused for the rest of that bucket. A true sliding window needs a sorted set and one ZREMRANGEBYSCORE per call; the bucket is cheaper, and a caller who times the boundary gets two bucketfuls back to back. Total-spend caps never catch a new abuser's first hour, because the total is still small while the derivative is vertical.

What does the middleware look like?

Fifty lines and an atomic counter.

# spend_gate.py - per-customer hard cap in front of any LLM provider.
# Public domain. Fork it. Prices USD per 1M tokens, verified 22 July 2026.
# List price; data-residency regional endpoints add 10%.
import time, redis
r = redis.Redis()

PRICE = {                          # (input, cached_input, output)
    "gpt-5.6-sol":  (5.00, 0.50,  30.00),
    "gpt-5.4-mini": (0.75, 0.075,  4.50),
    "gpt-5.4-nano": (0.20, 0.02,   1.25),
}

class Refused(Exception): ...

def _cost(model, t_in, t_cached, t_out):
    p_in, p_cache, p_out = PRICE[model]
    return ((t_in - t_cached) * p_in + t_cached * p_cache + t_out * p_out) / 1e6

def _apply(key, delta, ttl):
    total = r.incrbyfloat(key, delta)   # atomic: no read-then-write race
    if total < 0:                       # key aged out mid-run; never bank credit
        r.set(key, 0); total = 0.0
    r.expire(key, ttl, nx=True)         # own the window; re-arm if resurrected
    return total

def reserve(customer, run_id, model, prompt_tokens, max_output_tokens,
            run_ceiling=0.50, customer_budget=5.00, customer_window=86_400,
            velocity_ceiling=2.00, velocity_window=60, run_ttl=3_600):
    """Reserve-then-settle: debit a worst-case estimate before the call, then
    replace it with the metered actual after. max_output_tokens is the correct
    worst case - reasoning tokens are billed as output and counted inside it.
    Raise run_ttl above any run that can outlive it, or a long agent silently
    gets a second ceiling. These defaults refuse the $1.03 worked example on
    call one."""
    worst  = _cost(model, prompt_tokens, 0, max_output_tokens)
    bucket = int(time.time() // velocity_window)   # fixed bucket, not sliding
    gates = [(f"run:{run_id}",            run_ceiling,      run_ttl),
             (f"cust:{customer}",         customer_budget,  customer_window),
             (f"vel:{customer}:{bucket}", velocity_ceiling, velocity_window * 2)]
    held = []
    for key, ceiling, ttl in gates:
        total = _apply(key, worst, ttl)
        held.append((key, ttl))
        if total > ceiling:
            for k, k_ttl in held:
                _apply(k, -worst, k_ttl)           # release holds, then refuse
            raise Refused(f"{key} at {total:.2f} exceeds {ceiling:.2f}")
    return worst, held

def settle(reserved, held, model, usage):
    """usage is the Responses API object: cached tokens live at
    input_tokens_details.cached_tokens, never at the top level."""
    actual = _cost(model, usage.input_tokens,
                   usage.input_tokens_details.cached_tokens, usage.output_tokens)
    for key, ttl in held:
        _apply(key, actual - reserved, ttl)        # signed correction, no reset
    return actual

INCRBYFLOAT is atomic, so concurrent requests cannot all read the same under-ceiling value and all pass. expire(..., nx=True) needs Redis 7.0 or later. reserve returns (reserved, held) in the order settle takes them, so settle(*reserve(...), model, usage) is correct rather than silently transposed. A call that throws leaves its reservation debited — safe, but release it in a finally block if your ceilings are tight.

reserve implements three of the four controls. Model-tier routing lives in your call site: catch Refused on the strong model, retry once against the cheap one, refuse only if that trips too. Refusal is yours to shape: return 402, name the ceiling, set Retry-After, log the customer id. Parameter naming matters — max_output_tokens on the Responses API, max_completion_tokens on Chat Completions.

How does per-customer capping fail in production?

In named ways. Unnamed failure modes never get engineering time, and unbounded consumption is one of the categories I check before signing a go/no-go.

  • The advisory ceiling. A budget setting that sends email while the meter runs.
  • The org-wide kill switch. Credit exhaustion stops every customer to stop one.
  • The reasoning-token surprise. Reasoning tokens are billed as output and sit inside max_output_tokens, so a worst case computed without them is fiction.
  • The racing counter. Read-then-write instead of atomic increment. Twenty concurrent calls read the same value and twenty pass.
  • The resurrected counter. A settle that writes to a key whose TTL has already elapsed recreates the key with no expiry, at a negative value whenever the metered actual came in under the reservation. That customer never trips the cap again.
  • The long-run reset. A run key with a one-hour TTL gives a six-hour agent six ceilings.
  • The bucket boundary. A fixed velocity bucket is two bucketfuls to anyone who times the edge. Cheap, and worth saying out loud.
  • The eventually-consistent ledger. LiteLLM counts in Redis and reconciles the database behind it; general_settings.fail_closed_budget_enforcement: true buys an authoritative read for a database round trip per request.
  • The team-key blind spot. LiteLLM issue #12905, opened 23 July 2025 and closed via PR #32005: user budgets silently skipped whenever a virtual key belonged to a team.
  • The cap that never resets. LiteLLM issue #31292, open as of 22 July 2026: global max_budget ignores budget_duration and enforces a hardcoded trailing 30-day window that 429s forever.
  • Fail-open by omission. Counter store unreachable, so spend becomes unlimited, and nobody chose that.
  • The retry multiplier. A failed run retried is a run billed twice, which makes idempotent tool calls a cost control.

Model providers ship a synchronous volume control and an asynchronous money control, and never the two together. The counter that refuses the call is one somebody outside the provider has to run.

When is a soft alert enough instead of a hard cap?

When you are the one paying, the surface cannot loop, and a human reads the bill monthly. Everywhere else, cap it.

SOFT ALERT OR HARD CAP? - v1.0 (22 July 2026)
The Hopium Lab. Decide per surface, not per company. Fork it.

SURFACE                      WHO PAYS  LOOPS?  CONTROL
Internal tool, named staff   you       no      soft alert, daily digest
Internal agent, scheduled    you       yes     run ceiling + velocity breaker
Paid seat, usage on-bill     customer  no      soft alert + overage invoice
Paid seat, flat-fee plan     you       no      per-customer window budget
Free tier / trial / demo     you       no      hard cap, fail closed
Unauthenticated endpoint     you       n/a     hard cap + auth, or delete it
Customer-triggered agent     you       yes     all four controls
BYO-key (customer's key)     customer  yes     per-run ceiling only

TRIP RULES
[ ] Ceiling breached  -> refuse 402, name the ceiling, send Retry-After.
[ ] Velocity breached -> route to the cheapest tier that answers, then refuse.
[ ] Counter store down -> fail closed on unpaid surfaces, fail open on paid
       ones. Write the choice down. It is a product decision, not a default.
[ ] Legitimate heavy user trips the breaker -> raise that customer's ceiling
       by hand. Never raise the default.

BEFORE YOU SHIP
[ ] Worst hour at list price, in writing:
       max concurrency x calls/sec x per-call worst case x 3600.
[ ] Name the person paged when a ceiling trips.
[ ] Confirm retries are idempotent. A retry storm bills twice.
[ ] Re-date the price table. Model lines are replaced quarterly.

The gateway objection is fair and has an answer. Yes, a gateway is another hop and another dependency, and fail-open versus fail-closed is a decision made per surface rather than a default inherited from a config file. The velocity circuit breaker will occasionally refuse a legitimate heavy user, which is what the trip rules are for.

The gateway argument is not scarce. Several pages ranking for the question make it well, and each one terminates in a gateway you can buy. Buy one if the operational load is worth the licence fee. The four controls are identical either way; only the place you enforce them changes.

Ross Jones, Founder, The Hopium Lab. Last modified 22 July 2026. Engineering commentary, not legal or financial advice. Prices retrieved 22 July 2026 and stale by the time you need them — re-check before relying on the arithmetic.