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, free-model requests are blocked with a 429 until the daily reset — switch to a paid model to keep going.
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.
Web search / fetch per dayper accountA daily allowance that scales with your plan (20 a day without one), counted per account rather than per key, and counted separately for searches and page reads. On a paid plan, running out returns 429 with the time until it frees up; without a plan, further units are billed to your wallet instead.
Web requests per minuteper planA tighter burst limit than the general per-minute limit, because each web request consumes scarce upstream capacity rather than just tokens. 10/minute on Pro, up to 60 on Power.
Concurrent web requestsper planA cap on simultaneous web requests. A per-minute limit cannot see how many are still open, and each one holds a connection for several seconds.

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.

Was this page helpful?