---
title: "Rate limits · API docs · CheckAnySite"
description: "How many requests a key can make each minute, the headers that report it, and when to retry."
url: "https://docs.checkanysite.com/rate-limits"
---

Concepts

# Rate limits

How many requests a key can make each minute, the headers that report it, and when to retry.

## The limit

Each key can make 60 requests a minute, whatever they ask for. The limit applies to each key separately, so two apps with their own keys don’t slow each other down. Credits, by contrast, are shared by all of an account’s keys ([Credits](https://docs.checkanysite.com/credits)).

## Rate limit headers

Every response to a request with a key includes these headers:

| Header | Value |
| --- | --- |
| `X-RateLimit-Limit` | The requests a key can make in one window: 60 |
| `X-RateLimit-Remaining` | The requests left in the current window |
| `X-RateLimit-Reset` | When the current window ends, in Unix seconds |

## Exceeding the limit

A request over the limit returns `429` with the code [rate\_limited](https://docs.checkanysite.com/errors#rate_limited) and a `Retry-After` header that gives the seconds to wait. The request costs nothing.

```text Response over the limit
HTTP/2 429
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1762866060
Retry-After: 12

{"error": {"code": "rate_limited", …}}
```

Note

Several limits return `429`, so branch on the error’s `code`, not only its status. `quota_exceeded`, `key_limit_reached` and `scan_limit_reached` mean a monthly or daily limit is used up: retrying fails the same way until the limit resets.

## When to retry

| Status | Code | Retry? |
| --- | --- | --- |
| 409 | [idempotency\_key\_in\_use](https://docs.checkanysite.com/errors#idempotency_key_in_use) | Yes, with backoff |
| 429 | [rate\_limited](https://docs.checkanysite.com/errors#rate_limited) | Yes, after Retry-After |
| 429 | [quota\_exceeded](https://docs.checkanysite.com/errors#quota_exceeded) | No |
| 429 | [key\_limit\_reached](https://docs.checkanysite.com/errors#key_limit_reached) | No |
| 429 | [scan\_limit\_reached](https://docs.checkanysite.com/errors#scan_limit_reached) | No |
| 500 | [internal\_error](https://docs.checkanysite.com/errors#internal_error) | Yes, with backoff |
| 503 | [capacity](https://docs.checkanysite.com/errors#capacity) | Yes, after Retry-After |
| 4xx | Any other | No. Change the request first; the message says what to change |

- Back off between attempts: wait 1, 2, 4 and 8 seconds, with a little random jitter, for up to five attempts. When the response has a `Retry-After` header, wait that long instead.
- Polling a scan is free and doesn’t use credits. Read its `links.self` as often as `Retry-After` asks, about every 3 seconds.
- Send the same `Idempotency-Key` with every attempt of a POST request, so a retry never starts or charges for anything twice ([Idempotency](https://docs.checkanysite.com/idempotency)).

The following function retries a request this way:

```javascript
const RETRYABLE = new Set([
  'idempotency_key_in_use',
  'rate_limited',
  'capacity',
  'internal_error',
]);

async function callApi(path, init = {}, tries = 5) {
  // Send the same key with every try of a POST, so a retry never starts
  // or charges for a second scan.
  const idempotency =
    init.method === 'POST' ? { 'Idempotency-Key': crypto.randomUUID() } : {};
  for (let attempt = 1; ; attempt += 1) {
    const response = await fetch(`https://api.checkanysite.com/v1${path}`, {
      ...init,
      headers: {
        Authorization: `Bearer ${process.env.CHECKANYSITE_API_KEY}`,
        ...idempotency,
        ...init.headers,
      },
    });
    if (response.ok) return response.json();
    const { error } = await response.json();
    if (!RETRYABLE.has(error.code) || attempt === tries) {
      throw new Error(`${error.code}: ${error.message} (${error.requestId})`);
    }
    // Wait as long as Retry-After asks, or back off: 1, 2, 4… seconds.
    const retryAfter = Number(response.headers.get('retry-after'));
    const seconds = retryAfter || 2 ** (attempt - 1);
    const delay = seconds * 1000 + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

const site = await callApi('/sites/wikipedia.org?checks=ai');
```

The [Python SDK](https://docs.checkanysite.com/sdks/python) retries like this automatically.
