How to handle a 429 rate limit response: the limits on each plan, how to back off and retry, and what to do if you need more.
Understanding rate limits
Authenticated requests are limited per API key and per account plan. A shared platform throttle also protects overall service stability.
- Free: 10 requests/minute
- Starter: 60 requests/minute
- Professional: 300 requests/minute
- Business: 1,000 requests/minute
- Enterprise: 5,000 requests/minute
Implementing backoff
A 429 from the per-key or per-account limit carries Retry-After: the whole seconds until the current window ends, which can be up to a minute. Wait that long before retrying. X-RateLimit-Reset (Unix seconds) says the same thing as a timestamp. Only a 429 with neither header, such as one from the shared platform throttle, needs a guessed exponential delay:
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// How long a 429 asked us to wait, in milliseconds.
function waitFor(response, attempt) {
const retryAfter = Number(response.headers.get('Retry-After'));
if (retryAfter > 0) return retryAfter * 1000;
const reset = Number(response.headers.get('X-RateLimit-Reset')); // Unix seconds
if (reset > 0) return Math.max(0, reset * 1000 - Date.now());
return Math.min(60000, 2 ** attempt * 1000); // no hint: back off exponentially
}
async function verifyWithBackoff(email, maxAttempts = 5) {
for (let attempt = 0; ; attempt++) {
const response = await fetch('https://api.kawaa.com/v1/verify', {
method: 'POST',
headers: {
'X-Api-Key': process.env.KAWAA_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
if (response.status !== 429) {
// Any other failure (an invalid key, insufficient credits, malformed
// input, a 5xx) is an error envelope, not a verification result.
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error('Verify failed: ' + response.status + ' ' + (err?.error?.message || response.statusText));
}
return response.json();
}
if (attempt + 1 >= maxAttempts) {
throw new Error('Still rate limited after ' + maxAttempts + ' attempts');
}
// A little jitter, so many clients do not all retry on the same second.
await sleep(waitFor(response, attempt) + Math.random() * 1000);
}
}Retrying POST /v1/verify on a 429 is safe: the per-key and per-account limit, and the platform throttle, refuse a request before anything is charged. Not every 429 is that early. One that carries retry_may_charge_again: true came after a charge (for example from POST /v1/compose when the AI service is busy), so check GET /v1/usage/credits/transactions before repeating that request.
Best practices
- Use batch endpoints instead of single requests
- On a 429, wait for
Retry-After; fall back to exponential, jittered delays only when it is missing - Queue requests and process at a sustainable rate
- Cache results to avoid re-verifying same emails
Need higher limits?
Upgrade your plan or contact sales for custom limits.