Skip to main content

Troubleshooting

Rate limit exceeded

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.

Something wrong or missing here?

Tell us what to fix. Your email goes to the support team with this article's title already filled in.

Send feedback

Still need help?

Email support@kawaa.com. If your question is about a bulk job, include its job ID from the dashboard.

Contact support