Skip to main content

Overview

Every API request is rate-limited based on the type of operation. Limits are applied per authenticated user (or per IP for unauthenticated requests) using a sliding window algorithm.

Rate Limit Tiers

TierLimitWindowTypical Endpoints
read300 requests1 minuteGET endpoints (list, retrieve)
write30 requests1 minutePOST, PUT, PATCH, DELETE
ai5 requests1 minuteAI quiz generation
render10 requests1 hourVideo render requests

Response Headers

Every successful response includes rate limit headers:
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp (seconds) when the window resets
Example response headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
X-RateLimit-Reset: 1742083260

Handling 429 Responses

When you exceed the rate limit, the API returns a 429 Too Many Requests response in RFC 9457 Problem Details format with a Retry-After header:
{
  "type": "https://quizquail.com/problems/rate-limit-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Try again in 12 seconds."
}
Response headers on 429:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 12

Best Practices

Use the Retry-After Header

Always respect the Retry-After header value. It tells you exactly how many seconds to wait before retrying.

Implement Exponential Backoff

For robust integrations, combine Retry-After with exponential backoff:
async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    const retryAfter = parseInt(response.headers.get("Retry-After") || "1", 10);
    const backoff = retryAfter * 1000 * Math.pow(2, attempt);

    console.log(`Rate limited. Retrying in ${backoff / 1000}s...`);
    await new Promise((resolve) => setTimeout(resolve, backoff));
  }

  throw new Error("Max retries exceeded");
}

Monitor Your Usage

Check X-RateLimit-Remaining proactively. When it drops below 10% of your limit, slow down requests to avoid hitting the ceiling.
function shouldThrottle(response) {
  const limit = parseInt(response.headers.get("X-RateLimit-Limit"), 10);
  const remaining = parseInt(response.headers.get("X-RateLimit-Remaining"), 10);
  return remaining < limit * 0.1;
}

Batch Where Possible

Reduce request count by using list endpoints with appropriate limit values instead of fetching resources one at a time. See Pagination for details on efficient data retrieval.