Rate Limiting

Per-key rate limits on the Content API, the separate limit on the Agent Platform account routes, the headers to read, and how to handle 429.

After this guide an agent stays inside its rate limits and recovers from a 429 without losing work or paying twice. There are two limits: one per Content key on the Content API, and one per account on the Agent Platform account routes.

Content API: per key

On the Content API, each key has its own budget, rateLimitPerMinute, counted over a 60-second window and shared by every route the key calls. One busy key never uses up another key's budget. Keys that registration and the Agent Platform dashboard create get the default, 60 requests per minute. Requests refused with 401, or with 403 for a missing scope or classroom, do not count.

Every counted answer carries:

HeaderMeaning
X-RateLimit-LimitThe key's rateLimitPerMinute.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetSeconds until the window resets.
Retry-AfterOnly on 429: seconds to wait.

Over the limit, the answer is 429:

JSON
{
  "error": {
    "code": "content_rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 12 seconds",
    "retryAfterSeconds": 12,
    "status": 429,
    "requestId": "..."
  }
}

The full rules, including the limit on failed authentication, are in Limits.

Agent Platform account routes: per account

The account routes (/v1/platform/agent/*, /v1/platform/health, LTI registrations) are limited per account (per workspace), with every key of the account sharing one pool. The limit is rateLimitPerMinute on GET /v1/platform/agent/account, 1,000 requests per minute by default. Over it, the answer is 429 platform_rate_limit_exceeded.

Two public routes are limited per client IP, not per account:

RouteLimit
POST /v1/platform/agent/register5 attempts per IP per rolling hour. Failed attempts, such as a 422, count too.
GET /v1/platform/share-redirects/{shareToken}60 requests per minute per IP

Every answer from these routes carries X-RateLimit-* headers. Over the limit the answer is 429 platform_rate_limit_exceeded, with a Retry-After header and the same wait in error.retryAfterSeconds:

JSON
{
  "error": {
    "code": "platform_rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 1800 seconds",
    "retryAfterSeconds": 1800,
    "status": 429
  }
}

Register once and keep the key. An agent that registers on every start runs out of attempts within the hour. Clients that share an outbound IP, such as the hosted MCP server, share these limits.

Handle 429

Wait for Retry-After, then send the same request again with the same Idempotency-Key:

JavaScript
// Node.js 18+
async function fetchWithRateLimit(url, init, maxAttempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(url, init)
 
    if (response.status !== 429 || attempt === maxAttempts) {
      return response
    }
 
    const waitSeconds = Number(response.headers.get('retry-after') ?? '1')
    await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000))
  }
}

Best practices

  • Watch X-RateLimit-Remaining and pause until X-RateLimit-Reset when it reaches 0.
  • Prefer one generation run for a whole course over many small calls.
  • Poll runs every 10 to 30 seconds, not faster, or use webhooks.
  • There is no batch evaluation route. Queue evaluations and keep their rate under the key's limit.
  • Contact TutorFlow if you need a higher limit.