Skip to content

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.

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.

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.