Sign in

Troubleshooting

Customer spending caps: troubleshooting

One entry per question about customer spending caps, with its cause and its fix.

On this page

One entry per real question, each with the cause and the fix. Start here before you change any code.

"Customer" on this page means your customer. Your own account with us is your organization.

The mechanism is Customer spending caps; the exact shapes are the wire.


The cap is not refusing anyone

Four causes, in the order they are worth checking.

  1. The hard stop is off. A cap with its hard stop off counts and refuses nothing; that is what it is for. Settings → Organization → Limits, the Customer spending caps section: the switch on the row should read Armed.
  2. The tag never arrived. Send one request and read the reply's headers. If x-recovea-budget-remaining is missing, no armed cap of that customer's was standing over the request. A reply carrying x-recovea-budget-scope with a value that is not customer, and no figures, is telling you the same thing: what stood over that request was a cap of your own.
    • The header name is x-recovea-tags, and the value starts customer:. Header names are not case-sensitive; the value is.
    • A client-wide default header is the commonest cause. Set once at construction (defaultHeaders, default_headers, a shared axios instance), it sends the same customer for everybody, so one customer reaches the cap and the rest never appear.
    • The tag must be on the call that goes through us. An embeddings call, a second client built elsewhere in your code, a background job with its own client: each needs the header on its own request.
  3. No armed cap for that customer. A customer cap lives on your organization; if there is no armed default rule and no armed override for that customer, there is nothing to refuse against.
  4. The plan. Customer spending caps are Team and above. On Free your organization sees the count of customers tagged this month and what they cost in total; setting, raising or arming a cap per customer needs Team.

A customer went over their cap

Cause: a cap stops the next request, not the ones already running. The request that crosses the line is admitted and priced when it finishes, and every request already in flight for that customer finishes too. On an expensive model one large request costs several dollars, so the overshoot is real: a request counts at its floor when it is admitted, and at its price only when it ends. A request the gateway cannot price is never held to the cap at its real cost: its floor counts while it runs and is released when it ends, apart from two cases (how the floor is set).

Fix, pick one:

  • Set the cap below the number you actually care about, by about the cost of your largest request.
  • Read x-recovea-budget-remaining on each 200 and stop the customer in your own code before we do. That is the only way to get a hard number, because your code knows what it is about to send and we do not.

The arithmetic is on Customer spending caps.


A customer was refused and I did not expect it

  1. Read the row. Settings → Organization → Limits → Customer spending caps. An override's row carries its cap and the modeled figure from the record. The Customers this period table under the default rule has a Stopped column: once a cap has stopped that customer it shows when, and what the enforcing node's counter read at that moment. A dash there means no refusal with a figure is recorded for that customer this period.
  2. Check which window. x-recovea-budget-window on the refusal names it, and x-recovea-budget-resets-at says when traffic resumes. A daily cap resets at 00:00 UTC, which is not midnight where you are.
  3. An override replaces the default rule for that customer on its own window, while it is armed. If you set a fifty-dollar monthly override on a customer who was under a five-dollar monthly default rule, the fifty dollars is what applies for the month once the override's hard stop is on; with the hard stop off the override only counts and the five-dollar rule still refuses. A default rule on a different window still applies on its own clock.
  4. A lower override behaves exactly like a higher one. An override set below the default rule refuses earlier, and nothing warns you that it is lower.
  5. The tightest of two default rules decides. Two caps on two clocks keep separate counts, so nothing adds them up: a five-dollar monthly rule and a one-dollar daily rule means a dollar a day.

I see budget_unverifiable

Cause: ours, and temporary. A node that could not confirm a customer's spend recently enough refuses rather than let spend run past a line you armed. It fails closed on purpose.

Fix: a retry, and nothing else. It answers 503 with Retry-After: 1. The official OpenAI and Anthropic SDKs retry a 503 by default, so with them your customer sees a slightly slower response rather than a refusal; a bare HTTP client retries only if you have told it to. Never show your customer a quota message for it. It is not their usage ending, and a message saying it was will be wrong the moment the retry succeeds. The status separates the two: budget_unverifiable is a 503 and budget_exceeded a 402 (the wire).


Zero customers seen

Cause: the tag is not reaching us. The count on the platform's Dashboard is a live count over the record, so zero means no tagged request landed.

How to see what you actually sent. Send one request with the header printed by your own client, then read the reply:

Shell
curl -i https://api.recovea.ai/v1/chat/completions \
  -H "Authorization: Bearer $RECOVEA_KEY" \
  -H "Content-Type: application/json" \
  -H "x-recovea-tags: customer:8812" \
  -d '{"model":"gpt-5-nano","messages":[{"role":"user","content":"hi"}]}'

Then check, in this order:

  • The prefix. customer:8812, not customer=8812, not customer 8812.
  • Commas. The header splits on commas first, so a comma ends the value. customer:8812, legacy is the customer 8812.
  • More than one customer: piece. The first one decides. If you build the header by concatenation, a leftover earlier piece wins silently.
  • Several header lines are fine: all of them are read as one list, so x-recovea-tags: team:payments on one line and customer:8812 on another both count.
  • Your own proxy. Anything between your app and us that drops unknown request headers drops this one.

A zero total beside a non-zero count is not this problem: the count is live off the record, so it says the tag arrived. The total is the sum of the customer cut's monthly rows other than untagged, and each request is added to its row as it is recorded; a request that could not be priced adds nothing to it.


A customer's requests were refused with a 400

JSON
{"error":{"message":"The customer tag must be 1 to 128 characters.","type":"invalid_request_error","param":"x-recovea-tags","code":"invalid_customer_tag"}}

Cause: the value after customer: was empty, longer than 128 characters, or not valid UTF-8, on an organization holding an armed customer spending cap. All three get this same body, so the sentence does not tell you which.

Fix: send an id of 1 to 128 characters, UTF-8 encoded. If your ids are longer, send a short stable key (your own account id) and keep the long one in your own system. If your ids are not plain ASCII, check what your HTTP client does to header values: some encode them as Latin-1, which we cannot read.

Why it is refused rather than shortened. A shortened id is a different customer: two ids sharing a 128-character prefix would collapse into one counter and the second customer would be refused on the first one's spend. We would rather refuse one request loudly than bill two customers as one quietly. On an organization with no armed customer cap the value is dropped and the request is counted with no customer, because there is no cap for it to be wrong about.

The body never echoes the value back.


Costs do not match my provider's invoice

Cause: they are two different records of the same traffic, and neither is wrong.

  • We price the token counts your provider reported from its published list prices, cached and uncached tokens alike, from a dated price list.
  • A request on a model that is not on that list is logged and counted as a request, and counts toward the cap only at its floor, never at a priced figure. So does a response we could not price at all. The floor counts while the request runs and is released when it ends, apart from two cases, and it can be more than the request costs (how the floor is set). The per-customer figures report the unpriced requests they record separately as unpricedRequests and never fold one into that row's spend total, and the counter also holds the floors of requests still running and any floor that stays or waits, so the cut and the counter are two different figures.
  • The price list in force is named beside the figures; when a month spans two lists, that field is null rather than naming one of them.
  • Your provider's invoice includes whatever they bill you for that we never see: other keys, other products, their own discounts and credits.

Fix: treat our figure as the one your caps run on, and the provider's invoice as the money you owe them. If the gap is large and not explained by the above, the per-customer unpricedRequests is the first number to look at.


Streaming replies

Both questions have the same answer: the headers arrive first.

  • The x-recovea-budget-* values are on the reply headers, which arrive before the first chunk, so your code can read the remaining dollars for that customer without waiting for the stream to finish. All five ride a stream served under an armed customer cap; the scope and the window alone ride one served under an armed cap of another kind.
  • A customer is never cut off mid-answer. A refusal is always a whole request. There is no mid-stream re-check, so a stream that started finishes.
  • A stream the client abandons is never priced: its floor counts toward the cap while it runs and is released when it ends, apart from two cases (how the floor is set), though your provider may bill what it sent before the client left.

My own plans renew on the customer's signup day

Cause: caps run on UTC calendar windows for every customer alike. A monthly cap resets on the 1st. A per-customer anniversary window is not offered.

The recipe:

  1. Give that customer a cap on the window that never resets.
  2. On each renewal, raise it by the dollars you include, in the platform: Limits → Customer spending caps → the override's Edit.
  3. Spend to date is kept when a cap is raised, because the counter is keyed by the customer rather than by the cap's row. Traffic resumes within about five seconds: a raised cap goes out on our control plane's five-second tick, and in a two-node test on a local estate on 2026-09-12 the gateway applied it 9 to 24 milliseconds after that tick.

A cap that never resets sends no x-recovea-budget-resets-at, not on a served response and not on a refusal, so what clears your own cache for that customer is your raise, not a time.

So a customer on a ten-dollar plan renewing on the 17th holds a cap that never resets, at ten dollars, then twenty, then thirty. The row shows how much of the lifetime figure they have used, which is also the answer to "how much have they ever cost me".


I want to turn one customer off

There is no per-customer off switch. What works: give that customer an override at the smallest cap we accept, one micro-dollar, on the window you are watching, and arm it:

Text
Monthly cap   0.000001

A cap of exactly zero is refused ("capUsd must be greater than zero."), because a zero cap is indistinguishable from a cap somebody forgot to fill in.

How soon it refuses depends on the size of the request body, and the threshold is about eight hundred bytes. The comparison adds a floor for the request in front of it to what the customer has already spent, and refuses if the two reach the cap. The floor counts the size of the request body in bytes divided by four, at the lowest rate the gateway's price list charges for any input token, cached or not, in whole micro-dollars rounded down, so:

  • A request body of about eight hundred bytes or more has a floor of at least one micro-dollar, which one micro-dollar of cap cannot fit at any spend. It is refused from the first request.
  • A body under about eight hundred bytes has a floor of nothing, so while that customer's counter still reads zero that one request is admitted. Once it is priced, the counter is over the cap and every request after it is refused. A minimal chat body is around sixty bytes, so a short call can see exactly one request through, not only a smoke test written to be as small as possible.
  • A customer who has already spent anything in that window is refused whatever they send: the comparison starts from their counter, and one micro-dollar of cap has no room left in it.

Either way a customer whose requests the gateway can price is stopped within one request: the reply to even a sixty-byte prompt prices the counter past a micro-dollar. On a model the gateway's price list does not carry, a body under about eight hundred bytes has no floor and adds nothing when it ends, so it is admitted every time while the counter still reads zero.

Every other customer is untouched, your provider is not called for the refused request, and deleting the override puts that customer back under your default rule. What that customer's app receives is a 402 saying their budget was reached, not that you turned them off, so if the message matters, say it in your own product.


Where is the email when a customer is refused

There is none, by design. A default rule over five thousand customers would otherwise mail you thousands of times in a bad hour, and we never contact your customers at all. Alert percentages saved on a customer cap send no mail either; threshold alerts are for your organization's cap and your keys' caps.

What you get instead:

  • Your own code sees the 402 the moment it happens: that is the fastest signal there is, and the only one that can reach the customer in the product.
  • x-recovea-budget-remaining on every 200 served under that customer's armed cap is the warning before the line, at whatever percentage your code chooses.
  • The platform shows each customer's cap and the modeled figure from the record, and each refusal is kept in the record: the Stopped column says when a cap last stopped that customer and what the counter read.

A customer was refused, then a request went through

Cause: convergence, and it is a few seconds. The counters live in memory on every node that admits your traffic and are exchanged between nodes on a short tick, so for a moment after a customer crosses the line one node knows and another does not.

In a two-node test on a local estate on 2026-09-12: after one node refused, the other refused between 0.7 and 5.0 seconds later, about 2.8 seconds at the median over twenty runs: one checkpoint tick, and the crossing lands wherever it lands inside one. Rarely it is longer: when both nodes publish in the same instant it can take well over a minute. That is the rare tail, not the shape.

Spend from all our gateway nodes is combined by our control plane from the priced record on a five-second tick, and what comes back can only raise a node's counter, never lower it. So every wait is in the one safe direction: a node that has not heard yet keeps serving, which means it refuses late and never early.

Fix: treat the first budget_exceeded for a customer as that customer's state until x-recovea-budget-resets-at passes or you raise their cap. Cache customer → resets-at on that first refusal and answer from your own cache, and you will not see the window at all. We deliberately hold no sticky latch on our side: a latch would keep refusing a customer whose cap you had just legitimately raised.


After a deploy, a customer's figure looks different

Cause: the enforcing counter and the record are two different numbers, and only one of them is durable. The counter a node compares against is in memory and is reconciled from priced events; the record is the receipts, and the receipts do not move when a process restarts.

How to check: read the per-customer figures for the period, either the Customers this period table on Limits or the cut on the wire. That figure comes off the record. The override row prints the modeled figure from the receipts, modeled ... (receipts, this window), and the Stopped column prints what the counter read when it last stopped someone, because they are different numbers with different provenance and printing one of them twice would hide that.

The receipts are the number to bill from.


See also