The token bucket
- : the bucket size, which is the burst allowance.
- : the refill rate, which is the sustained limit.
A request takes one token and is allowed if one is available. The bucket refills continuously, so idle time accumulates into permission to burst.
Those two numbers are the whole policy, and they are independent: a bucket of 100 refilling at 10 per second allows a burst of 100 and then 10 per second forever.
A fixed window counter cannot express that, and it also allows twice the limit across a window boundary, which is why the bucket is the standard.
The two limits that bind on a model API
Model APIs limit requests per minute (RPM) and tokens per minute (TPM).
Token limits are usually the binding one, and they are charged on input plus expected output, which means the limiter must estimate the completion before it runs.
Setting a large maximum output therefore reduces throughput even when the responses are short, because the reservation is taken up front.
Reading the headers instead of guessing
Responses carry X-RateLimit-Remaining, X-RateLimit-Reset and Retry-After headers.
The server publishes its own state, and a client that reads those headers can slow down before it is refused rather than after.
A 429 means back off, not retry at once.
A retry that ignores the reset time makes the overload worse and can extend the penalty.
One client, in order
- Keep a local bucket sized slightly below the published limit.
- Take tokens before issuing the request, and wait rather than fail when empty.
- Update the local state from the response headers after every call.
- On a 429, honour Retry-After, or back off exponentially with jitter if absent.
- Log rejections separately from errors, because they mean capacity rather than failure.
Queueing, and what it costs
Little's law says the wait is the queue length over the arrival rate, so a queue absorbs bursts by converting rejections into latency.
If the sustained arrival rate exceeds the refill rate, the queue grows without bound and every request eventually times out.
A bounded queue with a rejection at the tail is therefore correct, and an unbounded one is a way of failing later and worse.
Priorities help when they reflect a real difference. An interactive request and a batch job in the same queue means the user waits behind work that nobody is watching.
More keys do not mean more capacity.
Splitting traffic across keys usually shares one account-level limit, so it adds complexity and no throughput.
Higher limits come from the provider, and the code's job is to use what exists without wasting it.