Skip to content

Rate limits ​

What is limited, what is not, and what a 429 looks like.

The API ​

/api/v1 is limited per API key, not per organization and not per IP.

Default300 requests per minute per key
Overridden byYour plan's api_rate_limit_per_minute, when the plan states one
Unauthenticated60 per minute per IP — only reachable before a key is resolved
Keyapi_key:<key id>

Keying on the key rather than the IP is deliberate: several tenants can share an egress IP, and one noisy customer must not throttle another. The consequence worth knowing: an organization with three API keys gets three separate 300/minute buckets. If you are splitting traffic across services, split the keys too.

Headers ​

Every response carries:

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 292

A 429 adds:

Retry-After: 37
X-RateLimit-Reset: 1735689637

Retry-After is in seconds. Honour it rather than backing off on a schedule of your own — it is the exact time to the bucket reset.

The 429 body ​

json
{
  "error": {
    "type": "rate_limit_error",
    "message": "Too many requests. Slow down and retry after the interval in the Retry-After header.",
    "retryable": true,
    "documentation_url": "https://docs.callagent.dev/api/rate-limits",
    "request_id": "01JQ…"
  }
}

Handling it ​

ts
async function call(path: string, init: RequestInit, attempt = 0): Promise<Response> {
  const response = await fetch(`${base}${path}`, init);
  if (response.status !== 429 || attempt >= 5) return response;

  const retryAfter = Number(response.headers.get('Retry-After') ?? '1');
  await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
  return call(path, init, attempt + 1);
}

Two things this gets right that a naive retry does not: it uses Retry-After instead of guessing, and it caps the attempts so a permanently throttled key produces an error rather than a hang.

Watch X-RateLimit-Remaining and slow down before you hit zero. A client that only reacts to 429 spends part of every minute throttled.

The other limiters ​

LimiterApplies toLimitKeyed on
apiEverything under /api/v1, and POST /mcp300/min (or your plan's)the API key
internalThe gateway-to-control-plane channelnone—
webhooksInbound signed provider callbacks (Twilio, Telnyx, Stripe)1000/minthe source IP
authLogin and password reset5/minlowercased email + IP
demo-showcaseThe public sales demo600/minthe source IP

The internal channel is deliberately unlimited: it reports hundreds of events per call, and throttling it would drop call history rather than protect anything.

The webhooks limiter is for callbacks arriving at CallAgent. Outbound webhook delivery to your endpoint is not rate limited — it is governed by retry and backoff.

Limits that are not request limits ​

These stop a call rather than a request, and they are not 429s:

LimitWhere it bitesWhat you see
Concurrencyincluded_concurrency + burst_concurrency, plus per-campaign, per-number and per-trunkended_reason: concurrency_limit
Daily spend capBefore a dialended_reason: insufficient_credit
Plan volume ceilingsMonthly minutes and callsended_reason: insufficient_credit
Destination capsHourly, daily and daily-minutes per destinationended_reason: destination_blocked
Queue capacitymax_waiting_calls on a queue, default 100the queue's overflow destination
Plan entitlementsAssistant count, and similarHTTP 402 plan_limit_exceeded, with limit and used

Note the 402: a plan ceiling is a commercial limit, and returning 429 for it would tell a client to retry something that will never succeed.

A per-tenant override table exists and does nothing

rate_limit_overrides has organization_id, bucket, per_minute, burst, reason and expires_at columns, and no code reads it. The only override that works is the plan's api_rate_limit_per_minute. Do not plan around per-bucket custom limits.

Provider rate limits are separate ​

A 429 from OpenAI, Deepgram or ElevenLabs is theirs, not ours. It surfaces as:

json
{ "error": { "type": "provider_error", "code": "provider_rate_limit",
             "provider": "deepgram", "retryable": true } }

with HTTP 429 and a Retry-After: 5. On a live call the fallback chain handles it instead: the stage moves to the next provider and emits provider.fallback with reason: rate_limit. Configure a fallback for any provider you rely on.

Next ​

Documented against the code. Anything not yet implemented is labelled, never described as working.