Skip to content

Rate limits

To keep one busy or buggy program from using up all the capacity, the API caps how many requests each account can make per minute. Normal use — the web app, a well-behaved agent — never comes close to the cap, so most integrations never see one. This page is here for when you're running something heavier and want to pace it well.

Keyed by account

The cap is counted per account, using the account's uid (its unique id). Your login token (JWT) and any API keys all resolve to the same account, so they share one budget:

  • Two keys for one account share one budget — they don't each get their own.
  • Two accounts are isolated — one account's traffic never eats into another's.

Tiers

Different kinds of calls get different limits, because they cost us different amounts. Each limit is a fixed window of one minute — the count resets at the top of each minute — and the numbers below are the per-environment defaults (they're configurable, nothing is hardcoded). If a specific endpoint sets its own limit, that wins over the group default.

SurfaceKeyed byDefault
Authenticated default (the whole /v1 authed surface)uid300 / min
Billable processing (/projects/{id}/process, /process-next-batch)uid20 / min
Status / event poll (…/status, …/events)uid120 / min
Webhook management (/v1/webhooks…)uid30 / min
Anonymous estimate (/v1/anonymous-estimate)caller IP10 / min
Anonymous page image (picker thumbnails)caller IP600 / min

The billable processing tier is deliberately the tightest, because each of those calls can spend real GPU money. Think of it as a second line of defense next to the wallet: the wallet (402 insufficient_credit) refuses work you can't pay for, and this rate limit caps how often you can ask on top of that — so an empty or abusive account can't hammer the expensive path.

The 429 response

When you go over a limit, the API replies with 429 Too Many Requests in the same RFC 9457 envelope as any other error, with the stable code rate_limited. It also sends standard back-off headers that tell you exactly how long to wait:

http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 12
RateLimit-Limit: 20
RateLimit-Remaining: 0
RateLimit-Reset: 12

{ "type": "https://kenly/errors/rate_limited", "title": "Too many requests",
  "status": 429, "code": "rate_limited",
  "detail": "You have exceeded the request rate limit. …" }

Both Retry-After and RateLimit-Reset are the number of seconds until the window resets. So when you get a 429, sleep that many seconds and try again:

javascript
async function call(url, options) {
  const res = await fetch(url, options);
  if (res.status === 429) {
    const seconds = Number(res.headers.get("Retry-After") ?? 1);
    await new Promise((r) => setTimeout(r, seconds * 1000));
    return call(url, options); // retry once the window resets
  }
  return res;
}

Even better, watch RateLimit-Remaining as you go and slow down before you hit zero — that way a bulk job paces itself and never trips the limit at all.

Distribution + fail-open

The counters live in Redis (a fast shared data store), so a limit holds no matter which API server handles your request. And it fails open: if Redis is ever unreachable, the request is allowed through rather than rejected. In other words, enforcement lapses for that one request, but availability doesn't — a rate limiter should never be the reason the API goes down. (On a bare local run or in the tests, an in-memory counter with the same per-minute behavior stands in.)

Source of truth: the rate-limit tiers and 429 shape are single-sourced in the repo at docs/api-contract.md §8; this page renders it for developers.