Skip to main content

Rate limits

Rate limits are applied per client, not per organization or IP address. Creating a second client to get around a limit won't help, because sustained limits are also tracked at the organization level.

Limits​

EnvironmentSustainedBurst
Sandbox100 requests/minute20 requests/second
ProductionPer agreement, from 1,000 requests/minute100 requests/second

Some endpoints are more expensive to run and have their own lower limits. Those limits are called out on the endpoint's documentation page. Card secret retrieval and report generation are the main examples.

Production limits are set during program setup. If you need more capacity, you can request an increase and include some detail about your traffic pattern.

Headers​

Every response includes your current rate-limit status:

RateLimit-Limit: 1000
RateLimit-Remaining: 943
RateLimit-Reset: 37
HeaderMeaning
RateLimit-LimitRequests permitted in the current window
RateLimit-RemainingRequests left
RateLimit-ResetSeconds until the window resets

Read these headers on every response, not just when a request fails. Starting to back off when you're at 10% remaining is much smoother than waiting until the limit hits zero.

When you exceed the limit​

HTTP/1.1 429 Too Many Requests
Retry-After: 12
{
"error": {
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"message": "Too many requests. Retry after 12 seconds.",
"doc_url": "https://developer.aletaplanet.com/api-reference/errors/error-code-catalog",
"request_id": "req_01HX8ZQ2K4M7P9RSTV"
}
}

Honor Retry-After. The value is calculated from your actual rate-limit window; it isn't just a placeholder telling you to try again later.

Backing off​

async function withBackoff(fn, maxAttempts = 5) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const retryable = err.status === 429 || err.status >= 500;
if (!retryable || attempt >= maxAttempts - 1) throw err;

const wait = err.retryAfter
? err.retryAfter * 1000
: Math.min(2 ** attempt * 250, 8000);
// Jitter matters: without it, every client that hit the limit together
// retries together, and the next window is exhausted instantly.
await sleep(wait + Math.random() * 250);
}
}
}

Retry 429 and server errors with backoff. Use the Retry-After value when the server provides one, and add some jitter so a group of clients doesn't all retry at exactly the same time.

Staying under the limit​

  • Use webhooks instead of polling. Polling a card list for a status change is the biggest source of avoidable traffic, and we'll push the event to you anyway.
  • Expand instead of following IDs when you only need a single object. One request is better than three.
  • Batch background work and spread it out instead of firing a reconciliation job as one large burst at midnight.
  • Cache data that doesn't change often. Program configuration and card product metadata are good examples.
  • Use reports for bulk history instead of walking through paginated lists.

Limits that are not rate limits​

A 429 means you've sent too many requests. It does not mean:

  • 402 insufficient_funds — the account is empty
  • 403 ip_not_allowed — the source address isn't permitted
  • 422 velocity_limit_exceeded — a spend control was triggered, not an API rate limit

The last one is especially important. velocity_limit_exceeded is a business rule about money, so retrying it is always the wrong thing to do.