- Docs
- Guides
- Rate limits
Rate limits
Read account-wide limit headers and handle 429 responses with Retry-After, bounded backoff, and jitter.
EnvoAPI applies a rolling Account Rate Limit to the Customer Account, not to one API Key. All active keys on that Account and every Lookup Request or retry share the same allowance. The limit comes from the Account’s current Access Limits; do not hardcode a limit in client code.
Lookup responses that reach rate-limit accounting expose these headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
The Account’s current rolling request ceiling. |
X-RateLimit-Remaining |
Requests remaining in the current rolling window. |
X-RateLimit-Reset |
Whole seconds until capacity resets. |
Retry-After |
On 429, the positive number of seconds to wait before another attempt. |
The 429 response
Section titled “The 429 response”A request over the Account Rate Limit is rejected with 429; it is not queued.
The response uses the standard error envelope and includes
Retry-After:
{ "error": { "code": "rate_limit_exceeded", "message": "The Account rate limit was exceeded.", "retryable": true, "details": [] }, "meta": { "requestId": "request-example" }}Treat the response headers as the current source of truth. Limits can change when Account terms change, and traffic from another key or worker affects the remaining allowance.
Retry with a bound
Section titled “Retry with a bound”Honor Retry-After, add a small random delay so concurrent workers do not
retry together, and cap the attempt count. All current customer lookup
operations are GET, but every retry still consumes a rate-limit slot once it
is accepted and can incur a Lookup Cost.
const delay = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds));
export async function fetchWithRateLimitBackoff( url: URL, attempts = 3,) { for (let attempt = 0; attempt < attempts; attempt += 1) { const response = await fetch(url, { headers: { accept: "application/json", authorization: `Bearer ${process.env.ENVO_API_KEY ?? ""}`, }, });
if (response.status !== 429) return response; if (attempt === attempts - 1) break;
const retryAfter = Number(response.headers.get("retry-after")); const seconds = Number.isSafeInteger(retryAfter) && retryAfter > 0 ? retryAfter : 2 ** attempt; const jitterMilliseconds = Math.floor(Math.random() * 250); await delay(seconds * 1_000 + jitterMilliseconds); }
throw new Error("Rate limited after bounded retries");}Coordinate concurrency across workers that share a Customer Account. Cache
reusable results when appropriate and slow sustained workloads instead of
looping on 429. See Pagination for bounding multi-page jobs
and Authentication for the relationship between keys and
Accounts.