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.
| 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
- 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 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 has its own limit: 60 requests a minute for each test key.
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 |
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 245
X-RateLimit-Reset: 1701878400Handling a 429
When a request would go over the limit, the API returns 429 Too Many Requests and does not spend credits:
{
"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:
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.
Staying under the limit
- Batch code lookups with
POST /api/v1/codes/batchwhen you have many codes. - Cache responses you reuse. Release decisions from
GET /api/v1/releases/{releaseId}/decisionsnever change for a givenreleaseId, 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 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.
Last updated on