Rate limits

Several independent limits apply. Each measures a different thing, so hitting one tells you little about the others.

The limits#

What is measured
FieldTypeDescription
Requests per minuteper planHow 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 dayper planDaily allowance for free-tier models. Past it, usage is charged to your wallet rather than blocked.
Spending windowper planSubscription plans cap spend over rolling windows (for example 5 hours and 7 days). This protects the plan price, not your wallet.
Concurrent speech requestsper accountA cap on simultaneous text-to-speech calls. Separate from requests per minute, which cannot see how many calls are still open.
In-flight image jobsper planHow 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#

429 Too Many Requests
{
  "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#

Respect Retry-After, then exponential backoff with jitter
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 429 to 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.