Skip to content

Rate limits

Section 9 · From docs/MCP_PRIVACY.md on GitHub

Each RateLimitConfig is a token bucket. A bucket starts full at burst tokens (default ceil(rate)), refills at rate / period tokens per second, and a call needs one whole token.

The limits that apply to a call are the policy's rate_limits plus those of every matching role and rule. A limit is skipped when its kinds do not include the call's kind, or when none of its tools globs match the primitive's name or URI. Buckets are keyed by limit and scope:

scope One bucket per
principal principal id (all matching tools share it)
tool tool name or URI (all callers share it)
principal_tool principal and tool
global the limit itself

Limits are checked after the access decision, so denied calls cost nothing. When several limits apply, every bucket is refilled and checked first, and a token is taken from each only if all of them allow the call. A call refused by one bucket costs nothing in the others: with a 5 per hour principal limit and a 1 per hour limit on hot, one hot call and three refused ones left four calls for other tools.

The refusal is an isError result for tools and a protocol error for resources, prompts and completions. From a policy with {"tools": ["get_asset"], "rate": 2, "per": "minute"}, the third call within a minute:

{
  "isError": true,
  "content": [{"type": "text", "text": "Rate limit exceeded for tool 'get_asset' (2/minute); retry in 30.0s"}],
  "_meta": {
    "cloudg/error_code": -31029,
    "cloudg/error_data": {"retry_after": 29.99, "limit": "2/minute", "scope": "principal_tool"}
  }
}

retry_after is in seconds.

Completions count as calls. completion/complete runs check_call with kind completion, so a limit with kinds: [completion] throttles them separately and a limit without kinds covers them too. A limit with kinds: [tool] or [prompt] does not. With {"rate": 1, "per": "minute", "kinds": ["completion"]} the second completion raises:

RateLimitedError: Rate limit exceeded for completion 'cloudg://assets/{+ref}' (1/minute); retry in 60.0s

Buckets live in memory, per Policy object, and reset on restart. policy.reset_rate_limits() empties them. strict's 240 calls per minute with a burst of 120 is easy to hit with an agent loop or a benchmark: the per-call benchmark in section 16 tripped it partway through 200 back-to-back calls.