Rate limits
The v1 API rate-limits per API key. Each response carries headers that
tell you exactly where you stand, and a 429 tells you when
to slow down. Honor the headers and you'll never be surprised.
Per-plan budgets
Budgets are enforced per key across two windows at once: a sustained 60-second window and a tighter 10-second burst window. The burst window stops you from spending a whole minute's budget in the first 100 ms; if either window is exceeded the request is rejected.
| Plan | Steady-state | Sustained (60s) | Burst (10s) |
|---|---|---|---|
| Free | 1 req/s | 60 req | 20 req |
| Starter | 10 req/s | 600 req | 120 req |
| Pro | 50 req/s | 3,000 req | 500 req |
| Agency | 200 req/s | 12,000 req | 1,500 req |
Higher-volume workspaces (Scale) get the Agency budget by default with per-workspace overrides provisioned by us — see requesting a lift below.
Response headers
Three headers are stamped on every v1 response —
200, 4xx, and 429 alike — so
you can track your budget without waiting to be throttled. They
describe the sustained (60s) window:
| Header | Meaning |
|---|---|
RateLimit-Limit | Your sustained budget — the request count allowed per 60s window. |
RateLimit-Remaining | Approximate requests left in the current window. |
RateLimit-Reset | Delta-seconds until the current window rolls over. |
These use the legacy unprefixed triple form (the same shape GitHub and Stripe emit), not the expired IETF structured-fields draft. Pin against these names.
When you're throttled
Exceeding either window returns 429 with an RFC 9457
problem document (rate-limit-exceeded)
and a Retry-After header:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 7
RateLimit-Limit: 600
RateLimit-Remaining: 0
RateLimit-Reset: 7
{
"type": "https://docs.leadrails.dev/errors/rate-limit-exceeded",
"title": "Rate limit exceeded",
"status": 429,
"detail": "Too many requests for this API key in the current window."
} Retry-After is the number of seconds to wait before
retrying. Always prefer it over guessing: it reflects whichever window
(sustained or burst) you tripped.
Backing off correctly
Honor Retry-After when present; otherwise back off
exponentially with jitter. Reuse the same Idempotency-Key on retries so a
retried POST/PATCH never double-applies.
async function callWithBackoff(request: () => Promise<Response>, maxAttempts = 5) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await request();
if (res.status !== 429) return res;
// Prefer the server's hint; fall back to exponential backoff + jitter.
const retryAfter = Number(res.headers.get("Retry-After"));
const backoffMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: Math.min(30_000, 2 ** attempt * 1000) + Math.random() * 1000;
await new Promise((r) => setTimeout(r, backoffMs));
}
throw new Error("Rate limit: exhausted retries");
} Requesting a lift
Need sustained throughput above your plan's budget? Contact us — we can raise the per-key budget for your workspace. Include your workspace name and the steady-state RPS you're aiming for. Upgrading your plan in Billing also raises the budget per the table above.
What's next
- Idempotency — make every retry safe.
- Error catalog — the full 429 problem shape.
- Authentication — budgets are per key; here's how keys work.