Skip to content
Finowl

Engineering

Handling API rate limits and 429 errors in Finowl integrations

Learn Finowl's 60 requests per minute limit, tell rate_limited, quota_exceeded, and provider_rate_limited apart, honor Retry-After, and pace requests.

A 429 Too Many Requests response is not always the same problem. With Finowl, three different situations share that status code, and each one needs a different response from your code. This guide explains the rate limit, shows how to tell the three 429 codes apart, and walks through pacing techniques that keep you from hitting the limit in the first place.

Finowl's rate limit at a glance

Finowl allows 60 requests per minute per account. A few details shape how you should plan around it:

  • It is per account, not per key. If you have several services or several API keys on one account, they share the same budget.
  • A batch counts as one request. POST /v1/gender/batch takes up to 50 items and uses one slot of the limit.
  • Going over returns 429 with rate_limited and a Retry-After header that tells you how many seconds to wait.

Sixty per minute works out to one request per second on average. That is plenty for interactive features like a signup check, and with batching it is enough to process large lists quickly. The rate limits documentation is the authoritative reference.

Three kinds of 429

Every Finowl error uses the same body shape, so the code field is how you tell them apart:

json
{ "error": { "code": "rate_limited" } }
Code What it means What to do
rate_limited Your account exceeded 60 requests per minute Wait Retry-After seconds, then resend
quota_exceeded Not enough credits to run the request Stop and add credits
provider_rate_limited Upstream capacity is temporarily constrained Back off and retry later

rate_limited

This is the classic rate limit. The request was rejected before it ran, so it was not charged, and resending the same request after the wait is safe. Read the Retry-After header rather than inventing a delay. It is expressed in seconds.

quota_exceeded

This one looks like a rate limit but is really a billing state: the account does not have enough credits for the request. Retrying will keep failing until credits are added, so treat it as a stop signal. Pause the job, notify whoever owns the account, and resume after a top-up. Your 100 free monthly credits are used first, then prepaid credits, and you can enable auto top-ups with a saved card to avoid surprises in production. See the pricing page for packs.

provider_rate_limited

This code means the limit was reached further upstream, in the data source behind Finowl Intelligence, not on your account. It is temporary and not something your own pacing can fully prevent. Back off with exponential delays and jitter, honor a Retry-After header if one is present, and cap the number of attempts. If it persists, slow the overall job down rather than retrying harder.

Reading Retry-After correctly

Retry-After can, in general HTTP, be either a number of seconds or a date. Finowl sends seconds. A robust client still parses defensively and falls back to a sensible default:

javascript
function retryAfterMs(response, fallbackSeconds = 5) {
  const value = response.headers.get("Retry-After");
  const seconds = Number(value);
  if (value !== null && Number.isFinite(seconds) && seconds >= 0) {
    return seconds * 1000;
  }
  return fallbackSeconds * 1000;
}

Add a small random amount, such as up to 250 milliseconds, on top of the header value when many workers might wake up at once. That keeps them from all retrying in the same instant and immediately tripping the limit again.

Preventing 429s with a client-side token bucket

Reacting to 429 responses works, but it is better not to trigger them. A token bucket on the client keeps your request rate under the limit by construction.

The idea is simple. A bucket holds up to N tokens and refills at a steady rate. Each request takes one token. If the bucket is empty, the request waits until a token is available. For Finowl, a refill rate of slightly under one token per second and a small capacity give smooth pacing with a little room for bursts.

javascript
class TokenBucket {
  constructor({ capacity, refillPerSecond }) {
    this.capacity = capacity;
    this.tokens = capacity;
    this.refillPerSecond = refillPerSecond;
    this.last = Date.now();
  }

  refill() {
    const now = Date.now();
    const elapsed = (now - this.last) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerSecond);
    this.last = now;
  }

  async take() {
    for (;;) {
      this.refill();
      if (this.tokens >= 1) {
        this.tokens -= 1;
        return;
      }
      const waitMs = ((1 - this.tokens) / this.refillPerSecond) * 1000;
      await new Promise((r) => setTimeout(r, waitMs));
    }
  }
}

const bucket = new TokenBucket({ capacity: 5, refillPerSecond: 0.95 });

async function finowl(path, body, attempt = 0) {
  await bucket.take();
  const response = await fetch(`https://api.finowl.co${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FINOWL_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (response.status !== 429) return response;

  const { error } = await response.json();
  if (error.code === "quota_exceeded") {
    throw new Error("Finowl credits exhausted; top up before retrying");
  }
  if (attempt >= 4) throw new Error(`Gave up after repeated ${error.code}`);

  const base = error.code === "rate_limited"
    ? retryAfterMs(response)
    : Math.min(30000, 1000 * 2 ** attempt);
  await new Promise((r) => setTimeout(r, base + Math.random() * 250));
  return finowl(path, body, attempt + 1);
}

const res = await finowl("/v1/gender/name", { name: "Kim", country: "KR" });
console.log(res.status, await res.json());

Run this as an ES module (for example, save it as pace.mjs) so the top-level await works.

Sharing the bucket across processes

A token bucket inside one process only protects that process. Because the limit is per account, several workers each running their own 60-per-minute bucket will collectively exceed it. You have a few options:

  • Give each worker a fraction of the budget, such as 20 per minute each for three workers.
  • Keep the bucket in a shared store like Redis so all workers draw from one pool.
  • Route all Finowl calls through a single queue consumer that owns the pacing.

The last option is often the simplest. It also gives you one place to handle quota_exceeded and to pause everything at once.

Leave some headroom. If interactive traffic, such as a breached-password check at signup, shares the account with a background job, reserve part of the budget for the interactive path so users never wait behind a bulk import.

Batching to save requests

The most effective way to stay under the limit is to send fewer requests. The gender batch endpoint takes up to 50 items in one request, and that request counts once against the limit.

Approach Items per request Max items per minute
Single lookups 1 60
Batch lookups 50 3,000

Batching does not reduce credit usage: each item still costs 1 credit. It does reduce request count by up to 50 times, which is what the rate limit measures. Here is the same idea in a single curl call:

bash
curl -X POST https://api.finowl.co/v1/gender/batch \
  -H "Authorization: Bearer $FINOWL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "id": "c1", "type": "name", "value": "Andrea", "country": "IT" },
      { "id": "c2", "type": "name", "value": "Andrea", "country": "US" },
      { "id": "c3", "type": "email", "value": "li.wei@example.com" }
    ]
  }'

A few habits help batches work well with the limit. Fill batches to 50 when you can, since a batch of three uses the same request slot as a batch of 50. Collect items for a short window in near-real-time pipelines, for example a second or two, and send them together. Handle item-level errors individually rather than resending the whole batch, because successful items in that batch were already charged. Batch enriching 100,000 contacts puts all of this together in a full job.

Gender predictions returned this way are statistical estimates of how a name is typically used, not how a person identifies, so keep them to aggregate analytics and low-stakes personalization.

Monitoring and alerting

Track 429 responses by code. A steady trickle of rate_limited means your pacing is slightly too aggressive; tighten the bucket. Any quota_exceeded should page someone or trigger an auto top-up. Spikes of provider_rate_limited point to upstream pressure and are a signal to slow bulk work temporarily.

For the broader question of which failures are safe to retry, especially 5xx responses that may already have been charged, see safe retries for paid APIs.

Frequently asked questions

What is Finowl's rate limit?

60 requests per minute per account. A batch request counts as one request regardless of how many items it contains, up to 50.

How do I tell a rate limit from running out of credits?

Both return 429, so check the code in the error body. rate_limited means slow down and retry after Retry-After, while quota_exceeded means you need more credits.

What does provider_rate_limited mean?

The limit was reached upstream rather than on your account. Back off with exponential delays and jitter, honor Retry-After if present, and cap your retries.

Do rate-limited requests cost credits?

No. A rate_limited request is rejected before it runs, so it is not charged and is safe to resend after the wait.

Does batching reduce my credit usage?

No. Each batch item still costs 1 credit. Batching reduces the number of requests, which is what the rate limit counts, so you can process up to 50 times more items per minute.

All articles