Rate limits
Several independent limits apply. Each measures a different thing, so hitting one tells you little about the others.
The limits#
| Field | Type | Description |
|---|---|---|
Requests per minute | per plan | How often you may call. A rolling window, not a clock-minute bucket, so there is no burst at the top of each minute. |
Free tokens per day | per plan | Daily allowance for free-tier models. Past it, usage is charged to your wallet rather than blocked. |
Spending window | per plan | Subscription plans cap spend over rolling windows (for example 5 hours and 7 days). This protects the plan price, not your wallet. |
Concurrent speech requests | per account | A cap on simultaneous text-to-speech calls. Separate from requests per minute, which cannot see how many calls are still open. |
In-flight image jobs | per plan | How many image jobs you may have queued at once, so one account cannot occupy the whole queue. |
Why more than one limit
Requests per minute counts calls in a period; it is blind to how many are still running. One hundred simultaneous streams are one hundred calls — well inside a per-minute limit, and still one hundred open upstream connections. Different quantities need different instruments.
What a rate limit looks like#
{
"error": {
"message": "Rate limit exceeded. Please slow down and retry.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}A Retry-After header is included when the wait is known. Honour it — it is a real figure, not a suggestion, and retrying earlier just consumes another attempt.
Backing off correctly#
async function callWithBackoff(body: unknown, attempts = 4) {
for (let i = 0; i < attempts; i++) {
const res = await fetch("https://api.xkiro.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.XKIRO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (res.status !== 429) return res;
if (i === attempts - 1) return res;
// The server knows better than any formula when it told us.
const retryAfter = Number(res.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 2 ** i * 1000 + Math.random() * 300;
await new Promise((r) => setTimeout(r, waitMs));
}
}Retrying immediately makes it worse
A tight retry loop keeps you at the limit permanently: every retry consumes the capacity that would have let the next real request through. Always wait, and always add jitter so many clients do not retry in lockstep.
Repeated violations#
Continuing to hammer the API after receiving 429 is treated as abuse and can trigger a temporary cooldown on the source IP. Normal usage never reaches this; a runaway retry loop does, which is the main reason to get backoff right.
Staying comfortably inside the limits#
- Queue on your side. A small worker pool with a concurrency cap is far more predictable than firing requests as they arrive and relying on
429to shape traffic. - Batch what you can. Ten short prompts in one request usually beat ten requests, and cost fewer prompt tokens overall.
- Cache aggressively. Identical prompts producing identical answers is the cheapest optimisation available.
- Poll politely. For image jobs, poll every few seconds with backoff — not in a tight loop.
- Use one key per service. Usage is attributed per key, so when you do hit a limit you can see which integration caused it.
For which errors are safe to retry at all, see Errors. For how xKiro protects you from being billed twice by a retry, see Idempotency & retries.
