# Rate limits
URL: https://backworkhealth.com/docs/rate-limits

Per-minute limits for each pack, the rate-limit headers, and how to handle a 429

Each organization has one per-minute request limit, set by its pack. Every API key and OAuth token in the organization shares it. For prices and credits, see [Pricing and credits](/docs/pricing).

| Pack    | Requests per minute |
| ------- | ------------------- |
| Credits | 60                  |
| Build   | 300                 |
| Scale   | 1,000               |

An organization with only its starter credits gets the Credits limit.

## How the limit is counted [#how-the-limit-is-counted]

* The window is a sliding one minute, not a calendar minute.
* A request uses as much of the limit as it costs in credits. Most requests cost 1; some cost up to 8. [Pricing and credits](/docs/pricing#request-costs) lists each endpoint. A call that counts as 6 uses 6 of the minute's limit.
* If the limiter's shared store is briefly unavailable, Backwork falls back to half the limit, and never less than 10 requests a minute, until the store is back. The headers show the limit in force.
* The [sandbox](/docs/sandbox) has its own limit: 60 requests a minute for each test key.

## Rate-limit headers [#rate-limit-headers]

Responses carry the organization's current window:

| Header                  | Description                                       |
| ----------------------- | ------------------------------------------------- |
| `X-RateLimit-Limit`     | The per-minute limit that applies to this request |
| `X-RateLimit-Remaining` | Requests left in the current window               |
| `X-RateLimit-Reset`     | Unix timestamp when the window resets             |
| `Retry-After`           | On a `429` only: seconds to wait before retrying  |

```http
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 245
X-RateLimit-Reset: 1701878400
```

## Handling a 429 [#handling-a-429]

When a request would go over the limit, the API returns `429 Too Many Requests` and does not spend credits:

```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded",
    "hint": "Wait the number of seconds in the Retry-After header, then retry",
    "docUrl": "https://backworkhealth.com/docs/errors#rate-limit-exceeded",
    "retryable": true
  }
}
```

Wait the number of seconds in `Retry-After`, then retry:

```javascript
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 = Number(response.headers.get('Retry-After')) || 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
  }
  throw new Error('Still rate limited after retries');
}
```

Retry only errors whose `retryable` field is `true`. A `402 BILLING_CREDITS_EXHAUSTED` is not retryable; see [Pricing and credits](/docs/pricing#when-credits-run-out).

## Staying under the limit [#staying-under-the-limit]

* Batch code lookups with `POST /api/v1/codes/batch` when you have many codes.
* Cache responses you reuse. Release decisions from `GET /api/v1/releases/{releaseId}/decisions` never change for a given `releaseId`, so cache them for as long as you use that release. Jurisdiction reference data changes rarely; a 24-hour cache is reasonable.
* Spread bulk jobs across the minute instead of sending them at once.

## MCP usage [#mcp-usage]

[MCP](/docs/mcp) tool calls use the same limit and the same credits as direct API calls. One tool call can make several API requests, and each is counted at its own cost. MCP traffic appears in the usage data with source `mcp` and, when available, the tool name.
