Rate limiting

Understand per-client request limits and how to handle them

The API enforces per-client rate limits to ensure fair usage and service stability.

Default limits#

Each client account has a default limit of 60 requests per minute. This applies across all endpoints and is tracked by client ID, not by IP address.

One HTTP request counts as one request, whatever it carries. A text moderation batch of up to 99 texts is one request, and each GET that reads a job or a batch is one request. Billing is separate: each item of a batch is billed as one text moderation job.

A second, per-second window caps bursts at one tenth of your per-minute limit, rounded up. That is 6 requests per second at the default of 60 per minute. Both windows must pass, so a burst of 20 requests in one second returns 429 even when your minute budget is untouched. Spread submissions out rather than firing them all at once.

Your specific limit depends on your plan tier and can be viewed in the dashboard.

Shared Registry checks also count against a separate daily check quota. See Daily check quota.

Failed authentication throttling#

Requests that fail authentication are throttled separately, by source IP rather than by client ID: after 50 failed attempts within a minute, the IP is locked out for the remainder of that window.

The lockout applies to every request from that IP, including ones carrying valid credentials. The API checks the throttle before it looks up your key, so a corrected key does not lift the lockout. Wait for the window to expire (up to 60 seconds) and retry.

Requests that authenticate successfully do not count toward the limit, so normal traffic never trips it on its own. Two things to be aware of:

  • If several clients share one outbound IP (a NAT gateway or corporate proxy), one misconfigured integration can lock out the others.
  • Because in-flight authentications are counted while they resolve, an IP is also limited to 50 concurrent authenticating requests.

Rate limit headers#

Every response carries rate-limit headers. Read them instead of guessing your limit. They are the only place your account's values are exposed. Because a request is counted against both windows, the headers report whichever of the two the API evaluated last; treat them as the tighter of your two current budgets.

Header Meaning
x-ratelimit-limit Maximum requests allowed in the current window.
x-ratelimit-remaining Requests left in the current window.
x-ratelimit-reset Seconds until the current window resets.
retry-after Seconds to wait before retrying. Sent on a 429 response.

Pace your client on x-ratelimit-remaining, and honour retry-after on a 429 rather than applying your own fixed delay.

Rate limit response#

When you exceed either window, the API returns a 429 status code:

JSON
{
  "error": "RATE_LIMITED",
  "message": "Rate limit exceeded",
  "statusCode": 429
}

Handling rate limits#

Use exponential backoff with jitter when you receive a 429 response:

JavaScript
async function requestWithBackoff(fn, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fn();
    if (response.status !== 429) return response;

    const delay = Math.min(1000 * 2 ** attempt, 30000);
    const jitter = delay * (0.5 + Math.random() * 0.5);
    await new Promise((resolve) => setTimeout(resolve, jitter));
  }
  throw new Error('Rate limit exceeded after retries');
}

Best practices#

  • Batch submissions rather than sending many requests in quick succession. To check several texts at once, send them in one text moderation batch and receive all their decisions in one webhook.
  • Use webhooks instead of polling job status repeatedly. See webhooks.
  • Pace your polling. The default limit is 60 requests/minute, so polling a single job every 5 seconds is comfortably within budget. Poll more slowly when you are tracking many jobs at once. Every poll counts against the same limit as a submission. See error recovery for a reconnect-and-catch-up pattern.
  • Poll the list endpoint, not each job. One GET /api/v1/jobs?status=queued,processing covers every outstanding job, whatever the count.