lbreeze

A rate limit and a spend cap should not return the same status code

429 means try again shortly. 403 means stop. Return 429 when someone has hit a monthly budget and every well-behaved client you serve will retry, politely and forever, against a wall.

Kosta
lbreeze · 9/20/2026

Two things can stop an API call: you are going too fast, or you have spent your money. They feel similar from the inside — both are the service saying no — and they are routinely given the same status code. They should not be, because a client cannot tell them apart, and the correct response to one is the worst possible response to the other.

What a retry loop is told

Every competent HTTP client, and every SDK worth using, treats 429 Too Many Requests the same way: wait, then try again. Usually with exponential backoff, often honouring Retry-After. This is correct behaviour and you want it, because a rate limit is a temporary condition. The whole meaning of 429 is you may have this, just not yet.

Now apply that to a monthly budget.

A customer sets a spend cap. They reach it on the 14th. Your endpoint returns 429. Their client does what it was built to do: waits two seconds, retries. Fails. Waits four, retries. Fails. Waits eight, sixteen, thirty-two.

Nothing about waiting fixes a spent budget. The cap will not lift for another seventeen days. You have handed a well-written client an instruction it will follow diligently and pointlessly, burning their CPU and filling their logs, until somebody notices. And the person who notices will reasonably assume your service is broken — because from where they are standing, it is behaving exactly like a service that is overloaded.

403 Forbidden says something different: this request is not going to succeed by being repeated. A client that sees 403 surfaces an error, and a human reads it. That is what you want, because a human is the only thing that can resolve a spend cap.

The rule

The question is not how severe the refusal is. It is whether time alone will fix it.

  • Will waiting fix it? 429, with Retry-After if you can compute it.
  • Will waiting not fix it? 403, with a body that says what will.

Read that way, most refusals sort themselves quickly. Per-second throughput limit: 429. Concurrency limit: 429. Monthly spend cap: 403. Suspended account: 403. Revoked key: 403. Quota that resets at midnight: 429 with a Retry-After pointing at midnight — long, but honest, and a client can decide whether to wait or fail.

Say which one it was

A status code is a category, not an explanation. Whichever you return, the body should name the specific limit:

{
  "error": "spend_cap_reached",
  "message": "This key has reached its monthly cap of £50.00.",
  "cap": "50.00",
  "currency": "GBP",
  "resets_at": "2026-10-01T00:00:00Z"
}

Three things make this useful. The machine-readable error lets a client branch without parsing prose. The message is the sentence a developer will paste into a support ticket. And resets_at answers the question they are about to ask anyway.

Note what is absent: any suggestion of retrying. The document and the status code agree with each other.

The failure worth avoiding above all

Worse than the wrong code is the right-looking one. If your endpoint returns 200 OK with {"error": "..."} in the body, nothing in the client's error handling fires. The retry logic does not trigger, the alerting does not trigger, and the error travels onward as if it were data — into a variable, a template, a database, a customer's screen. Errors that arrive dressed as success are the ones that get discovered a week later, by someone else, in production.

Why this matters more when you are billing

If you are running an endpoint people pay for by usage, the spend cap is not an edge case. It is the safety net that makes the product safe to adopt. A developer will point something at your API for the first time on the strength of being able to say: this cannot cost me more than the number I chose.

That promise is only as good as the refusal that enforces it. A cap that is enforced with 429 gets retried around by accident. A cap that is enforced with 403 stops, says why, and stays stopped — which is precisely what was promised.

Get this right and the cap becomes a reason to trust you. Get it wrong and it becomes the outage nobody can explain.