Skip to main content

Rate Limiting

The MCP server accepts 600 requests per minute per person and organization — the token's sub and org, not the token itself.

Switching tokens does not reset the budget: a new token, from a new OAuth authorization or a new personal token, of the same person in the same organization lands in the same bucket. The same person in another organization has a budget of their own.

A tool call is never cheaper than the route it mirrors: the request is counted once in the MCP's own bucket and, when it is a tools/call, again in the v1 API bucket (600 per minute), with the same person-and-organization key. Routes with a limit of their own apply it too: kadu_executions_search has another 30 per minute, like GET /v1/executions/search. This is deliberate — an agent sweeping listings in a loop must not get around the limit the same sweep would pay through the API.

Attempts with a refused token are throttled per IP, not per token: a token that does not verify cannot have a bucket of its own. A request with no token at all — every client's first step, before OAuth — does not count as an attempt.

When the limit is reached​

When the MCP bucket runs out, the server answers 429 — the JSON-RPC message is never processed:

HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1793280037
Cache-Control: no-store
Content-Type: application/json

{ "error": { "code": "TOO_MANY_REQUESTS", "message": "Rate limit exceeded." } }

Wait the Retry-After seconds before the next request. X-RateLimit-Limit and X-RateLimit-Remaining give the bucket size and what is left; X-RateLimit-Reset is when it renews, in seconds since the Unix epoch.

When the route bucket runs out, the HTTP request is a 200 and the tool result comes back isError with the API error, RATE_LIMITED — see Errors. Wait a few seconds before calling again. The API limits are in Rate limits.

Spending less​

An agent spends budget like your backend does, and also pays context for every row it brings back. Three habits solve almost everything:

  • Filter before paginating. kadu_executions_list { status: "failed", started_at_gte: "2026-10-01" } costs one request; sweeping every execution to filter in the model costs dozens.
  • Read only what is new. To follow a running execution, pass after_sequence to kadu_executions_events_list and after_id to kadu_executions_logs_list, instead of re-reading everything on each round.
  • Do not poll in a loop. An approval by link is checked when the person says they decided, not every few seconds.