Rate limits and budgets

How the gateway meters you: rate_limited versus budget_exhausted, the rate-limit headers, spend caps, and reading your live headroom.

Two separate controls can refuse a request with 429: a rate limit on how fast you send requests, and a budget on how much you spend. The body's code tells you which one you hit, and they need different handling. Your account's limits are shown in your Console, and GET /api/v1/limits returns them, with live headroom, to your code.

rate_limited or budget_exhausted

rate_limitedbudget_exhausted
What ran outRequest rate: too many requests in the current window.Allowance: the plan's monthly cap, its 12-hour burst pool, or the monthly mira request cap is spent.
What to doBack off and retry after Retry-After.Stop retrying. No immediate retry succeeds.
ClearsWhen the rate window resets.At reset_at, when the spent window resets.
Headroomrate_limits.chat on GET /api/v1/limitstoken_pool on GET /api/v1/limits

Spend caps refuse with 429 too, under their own codes: a monthly cap on a key (key_spend_cap_exceeded), a monthly cap on a workspace (workspace_spend_cap_exceeded), and the credit pool (credits_exhausted). Like budget_exhausted, they mean an allowance is spent, and they clear only at reset_at or when the cap changes. Every 429 carries Retry-After; on these codes and on budget_exhausted it counts down to reset_at, and retrying sooner fails the same way.

POST /api/v1/moderations never returns budget_exhausted: moderation is exempt from the subscription pool, so a spent allowance cannot switch it off. It can still return rate_limited or a spend-cap code.

Rate-limit headers

Metered responses carry the state of the bucket the route is metered on:

HeaderMeaning
X-RateLimit-LimitRequests allowed per window.
X-RateLimit-RemainingRequests left in the current window, never below 0.
X-RateLimit-ResetWhen the current window ends, as an absolute Unix time in seconds, not a duration.
Retry-AfterOn every 429, the seconds to wait. On a budget or spend-cap refusal, the seconds until reset_at.

The X-RateLimit-* headers always describe the per-minute request window, so a budget refusal can still show requests remaining.

The X-RateLimit-* headers are absent on a 401, because authentication runs before the limiter, and on a 403 from the key check (key_scope_forbidden, key_read_only). They are also absent when the limiter cannot reach its own storage; that request is served unmetered rather than refused.

# -D - prints the response headers; look for X-RateLimit-* and, on a 429, Retry-After.
curl -s -D - -o /dev/null https://api.cruxy.ai/api/v1/limits \
  -H "Authorization: Bearer $CRUXY_API_KEY"

Pace yourself before you reach 0: when X-RateLimit-Remaining runs low, slow down until X-RateLimit-Reset rather than waiting for a 429.

Read your limits

GET /api/v1/limits returns the limits that apply to your key, with live headroom. Every value comes from the same limiter and budget state that chat enforces, so remaining is exactly what your next request is checked against. The call costs nothing, writes nothing, and is rate-limited like any other route.

curl https://api.cruxy.ai/api/v1/limits \
  -H "Authorization: Bearer $CRUXY_API_KEY"

Everything comes from the key you send; nothing is read from the request. Which blocks appear depends on your billing bucket:

FieldMeaning
objectlimits.
bucketHow this key is billed, and so which kind of key it is: subscription (a signed-in credential, with token_pool), apikey (a developer API key, billed per token), or headless (billed from prepaid credits). Memory and standing rules are on by default only for subscription.
meteredtrue for the apikey bucket only, and omitted otherwise: no token pool applies, and every request is billed per token. A key or workspace spend cap still binds it.
tierYour account's subscription plan, which sizes token_pool. This is not a model tier.
rate_limits.chat.per_keyThe chat request-rate window for this key: limit, remaining, resets_at, and window_seconds. resets_at is null until a request opens the window.
rate_limits.chat.per_orgThe same window, for your whole organization.
rate_limits.chat.max_concurrent_requestsHow many chat requests your account may have in flight at once, across all three chat endpoints. One more is refused at once, not queued: 429 rate_limited with reason: concurrency and Retry-After: 1.
token_poolSubscription bucket only. The weighted-token budget behind budget_exhausted.
creditsHeadless bucket only. The dollar-credit pool: granted, adjustment, used, remaining, and resets_at.
key_spend_capPresent only when the key has a monthly spend cap: cap, spent, remaining, and resets_at, in USD.
workspace_spend_capPresent only when the key's workspace has a monthly spend cap, in the same shape.

token_pool has two windows, each with cap, used, remaining, resets_at, and a state of healthy, low, or blocked:

  • monthly: the calendar-month cap.
  • burst: the trailing 12-hour sliding cap.

It also carries an overall state, blocked_models, the unit (weighted_tokens), and mira, a monthly request cap on the mira tier with its own cap, used, remaining, resets_at, and state.

There is no tokens-per-minute limit and no per-model daily cap. The gateway enforces neither, so neither appears here.

Budgets and auto

A request with model: "auto" may be budget-downgraded to mira. Read cruxy.routed_tier on the response to see the tier that served it, rather than assuming the router's first choice; see Auto routing.

Next steps

Was this page helpful?