Three decisions determine whether the cap holds.
Pick one budget unit and one window. Dollars per hour is the most defensible choice because it is what you are actually billed for, it absorbs price differences between models, and it survives prompt length variation. Token counts mislead: a 4,000 token prompt on a strong model can cost more than a 40,000 token prompt on a cheap one. Hourly buckets reset at a fixed minute mark, weekly buckets need a rollover job, and monthly buckets are too slow to protect against a bad afternoon. Use two layers: a hard hourly cap (the emergency brake) and a monthly cap (the board-level number).
Use a synchronous counter with reservation. Redis with a Lua script or an atomic increment plus compare is the standard pattern. The rule is important: reserve estimated cost before the call, reconcile to actual cost after. Estimating from token count via the provider's tokenizer is close enough; overshoot on a call is capped at the estimate error, not at the number of concurrent callers. Reconcile by writing the difference back, positive or negative, and keep a running total of unreconciled reservations so a crashed worker cannot permanently inflate the bucket.
# Pseudocode: reserve-then-call with a hard hourly cap
def admit(est_cost_usd, cap_usd, reserve_ratio=0.85):
key = f"spend:{current_hour_bucket()}"
script = """
local spent = tonumber(redis.call('GET', KEYS[1]) or '0')
local reserve = tonumber(ARGV[2])
if spent + reserve > tonumber(ARGV[1]) then return -1 end
redis.call('INCRBYFLOAT', KEYS[1], ARGV[2])
redis.call('EXPIRE', KEYS[1], 7200)
return spent + reserve
"""
return redis_eval(script, key, cap_usd, est_cost_usd)
def route_strong(task):
admitted = admit(est_cost_usd=task.est_cost)
if admitted < 0:
return queue_for_later(task) # overflow goes to the queue
try:
result = strong_model_call(task)
reconcile(task.est_cost, result.actual_cost)
return result
except TransientError:
release(task.est_cost) # give the budget back
raise
Hold back a reserve. Do not let the counter climb to 100 percent of the cap. Set the admission threshold at something like 85 percent, so in-flight calls and estimation error land inside the budget. The remaining 15 percent absorbs stragglers. When spend crosses the reserve line, the router stops admitting new strong calls entirely and everything new goes to the queue.
The distinction between a soft cap and a hard cap matters here. A soft cap triggers alerts and lets traffic through; a hard cap refuses admission. Provider dashboards and seat-level spend limits are useful for governance and per-user fairness, since they resolve limits through a hierarchy of user, group, seat tier, and organization scopes (Claude spend limits API). They are not a substitute for a gate your own router controls, because they operate on a slower clock than a burst.
| Layer | Unit | Checked when | Failure if missing |
|---|
| Hard hourly cap | USD per hour | Before each strong call, synchronous | Burst overshoots budget |
| Reserve threshold | Share of hourly cap | Same check | Estimation error and in-flight calls blow the budget |
| Monthly cap | USD per month | Before admission, plus nightly reconciliation | A bad week compounds unchecked |
| Queue with max wait | Tasks | When admission is refused | Work is lost or errors reach users |
| Fallback policy | Per task class | At queue expiry | No defined behavior at the edge |