LeadRails

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