Platform
Rate limits
Per-key rate limits, the headers we return, and how to back off.
Every authenticated call to /api/v1 is metered by a per-key rate limiter. Your ceiling is
set by your app’s tier (or a per-app override), the limiter counts requests in a
fixed 60-second window, and each response tells you exactly where you stand through a set of
X-RateLimit-* headers. Exceeding the limit returns HTTP
429 Too Many Requests with a Retry-After header. A separate, coarser per-IP
throttle protects the authentication path itself.
How limits are applied
Limits are keyed on the API key, not the app or the owner. Two keys under the same app
each get their own independent budget, so you can isolate a noisy integration behind its own key. Because
the limiter identity is the key’s hash — never a request header — you cannot raise your ceiling by
spoofing X-Forwarded-For or X-Real-IP.
The counter uses a fixed 60-second tumbling window. The first request opens a window; every request in the next 60 seconds counts against the same window; once the window elapses the count resets to zero on the next request. It is not a sliding window, so a burst early in a window is fully replenished at the window boundary rather than gradually.
Buckets are also scoped per route. Your tier limit is applied independently to each
endpoint family — for example chat/completions, guilds, usage,
personas, memories, and each webhook-delivery operation maintain separate windows.
Spending your budget calling one endpoint does not consume another endpoint’s budget. Treat the number
in X-RateLimit-Limit as the per-route ceiling and read it from the response rather than assuming
an aggregate.
Per-route budgets are a convenience, not a contract. Do not architect around calling many distinct endpoints to multiply throughput — the split may narrow in future. Right-size your traffic to your tier limit and always honor the live headers.
Tiers and limits
An app’s tier determines its per-minute ceiling. If your app carries a positive per-app rate-limit override, that value wins over the tier default. Tiers and overrides are managed by Krallo staff; you cannot change them from the console.
| Tier | Requests per minute | Per route |
|---|---|---|
| free (default) | 60 | Yes |
| pro | 600 | Yes |
| enterprise | 6000 | Yes |
| Per-app override | Custom (when set > 0) | Yes |
An unrecognized or unset tier resolves to the free ceiling. The values above are the
platform defaults; the effective limit is always reported in the X-RateLimit-Limit header, which
is the single source of truth for your key at the moment of the request. Rate limits are distinct from
billing quotas — see Billing & quotas for how metered calls draw down
your token balance and free quota, which is enforced separately (and returns 402, not
429).
Rate limit headers
Every response from an authenticated /api/v1 endpoint that reaches the rate limiter carries the
three advisory headers below — including the 429 itself. Read them on every response and adapt
dynamically rather than hardcoding numbers.
| Header | Type | Meaning |
|---|---|---|
X-RateLimit-Limit |
integer | The maximum number of requests permitted in the current window for this key and route. |
X-RateLimit-Remaining |
integer | Requests left in the current window after accounting for this request. Never negative; clamped to 0. A value of 0 means this was your last permitted request until the window resets. |
X-RateLimit-Reset |
integer | The Unix time, in seconds since the epoch (UTC), at which the current window ends and the counter resets. This is an absolute timestamp, not a number of seconds to wait. |
Example headers on a successful response:
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1752598860
X-RateLimit-Reset is an absolute epoch timestamp. To turn it into a delay, compute
reset - now in seconds and clamp at zero. When you receive a 429, prefer the
Retry-After header (a plain number of seconds) as the authoritative wait.
Which responses carry the headers
The headers are attached the moment a request passes authentication and any required scope check and reaches the limiter. As a result they appear on:
- Successful responses (
2xx) from rate-limited endpoints. - Billing rejections (
402) that occur after the rate-limit check. - Rate-limit rejections (
429) — the headers reflect the exhausted window, soX-RateLimit-Remainingreads0.
They are not present on:
GET /api/v1/ping, the unauthenticated liveness probe — it never touches the limiter.- Pre-auth failures that short-circuit before the limiter runs: a missing or invalid bearer token (
401), a revoked or expired key (401), a disabled app or suspended owner (403), an insufficient scope (403), a wrong method (405), or the master switch being off (404). See Errors for these codes.
Exceeding a limit (429)
When you exceed your per-route ceiling within the window, the request is rejected with
HTTP 429 Too Many Requests. The response includes a
Retry-After header carrying the number of seconds to wait before retrying.
/api/v1/… → 429 Too Many RequestsHTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1752598860
The 429 body identifies the exhausted bucket and restates the retry delay:
{
"error": "rate_limited",
"hint": "Too many requests for bucket 'devapi.chat.completions'",
"retryAfterSeconds": 60
}
The 429 body is not the standard error envelope. Unlike other /api/v1
errors — which return { "error": { "code", "message" }, "meta": { "request_id" } } (see
Errors) — a rate-limit rejection returns a flat object where
error is the string "rate_limited" and there is no meta.request_id.
Branch on the HTTP status code to detect throttling. Do not assume every error body has
a nested error.code field; special-case 429 before parsing.
Retry-After is a fixed, conservative value (currently 60 seconds — the full window
length). Because the window is fixed, the precise reset moment is given by X-RateLimit-Reset;
you may wait until that timestamp instead of the full Retry-After if you want to resume sooner,
but honoring Retry-After is always safe.
The pre-auth per-IP throttle
Before any token is hashed or looked up, the authentication path applies a separate, coarse throttle keyed on the caller’s resolved IP address. Its job is to stop floods of missing, invalid, revoked, or expired tokens from hammering the key store — it is intentionally generous and only trips under abnormal volume, not normal multi-key use.
| Property | Value |
|---|---|
| Scope | Per client IP, across the whole /api/v1 auth path |
| Default ceiling | 600 requests per minute |
| Window | 60 seconds, fixed |
| On exceed | 429 with Retry-After: 60 |
This throttle runs before the per-key limiter, so its 429 uses the same flat
rate_limited body shape but does not carry the X-RateLimit-*
headers (those are only set once a valid principal is established). The client IP is derived from
X-Forwarded-For / X-Real-IP where present, falling back to the socket address.
Because this is a defensive limit on failed and repeated auth attempts, the fix is the same as any
429: back off and retry.
Handling limits in your client
Build throttling into your client rather than reacting only when requests start failing:
- Read the headers proactively. When
X-RateLimit-Remainingapproaches0, slow down before you hit the wall instead of pushing into a429. - On a
429, honorRetry-After. Sleep for at least that many seconds before retrying. It is the authoritative, safe wait. - Add exponential backoff with jitter for repeated failures, so a fleet of clients does not retry in lockstep and re-trigger the limit (the thundering-herd problem).
- Cap retries and surface errors. Give up after a bounded number of attempts and report the failure rather than retrying forever.
- Isolate workloads per key. Since budgets are per key, run bursty batch jobs under a dedicated key so they cannot starve your latency-sensitive traffic.
async function callKrallo(path, init, attempt = 0) {
const res = await fetch(`https://exe.krallo.com/api/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.KRALLO_API_KEY}`, ...init?.headers },
});
if (res.status !== 429) return res;
// Prefer Retry-After (seconds); fall back to exponential backoff with jitter.
const retryAfter = Number(res.headers.get('Retry-After'));
const backoff = Math.min(60, 2 ** attempt) + Math.random(); // seconds
const waitSec = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : backoff;
if (attempt >= 5) throw new Error('Rate limited: exhausted retries');
await new Promise((r) => setTimeout(r, waitSec * 1000));
return callKrallo(path, init, attempt + 1);
}
You can also gate outbound requests on the remaining budget before you spend it:
Node.jsconst remaining = Number(res.headers.get('X-RateLimit-Remaining'));
const resetAt = Number(res.headers.get('X-RateLimit-Reset')) * 1000; // epoch ms
if (remaining === 0) {
const waitMs = Math.max(0, resetAt - Date.now());
await new Promise((r) => setTimeout(r, waitMs));
}
Errors this returns
| Status | Body error | When |
|---|---|---|
| 429 | rate_limited |
You exceeded your per-key, per-route ceiling within the 60-second window, or tripped the pre-auth per-IP throttle. Retry after Retry-After seconds. |
All other failure modes on these endpoints — authentication, scope, billing, validation — are covered on the Errors page. See also Authentication for key lifecycle and Billing & quotas for how metered usage is accounted separately from rate limiting.