Docs/Start/Rate limits

Rate limits

Per-plan request ceilings, the headers that report them, and how to back off correctly — including the two very different failures that both answer 429.

Two separate ceilings apply to every account, and they fail in the same way but mean opposite things:

  • A rate limit — how many calls per minute. Hitting it means slow down; the call would have succeeded a moment later.
  • A credit allowance — how many calls per billing cycle. Running out means the month is spent; retrying will not help until you top up or the cycle renews.

Both answer 429. Telling them apart is the single most useful thing on this page, and it is covered under which 429 is this.

The per-minute ceiling

PlanRate limitConcurrent callsCredits / month
Free5/min1100
Starter60/min5100,000
Pro180/min20500,000
MegaNo limit502,000,000

Rate limit is calls per minute, measured per API key. Concurrent calls is a separate ceiling — how many may be in flight at the same moment — and it is the one that bites first when you fan out. Ten parallel workers on a Starter key trip its concurrency ceiling of 5 long before they get near its 60/min limit.

Where the table reads No limit, there is no per-minute ceiling at all. That is not the same as unlimited: the credit allowance and the concurrency ceiling still apply.

A rate limit is a shape, not a total

A per-minute limit is not a bucket you empty and then wait on. The limiter refills continuously, so a steady stream spread across the minute never trips a ceiling that the same number of calls fired in one second usually does. Spacing calls evenly is worth more than counting them.

Your own lower limit

A key can be capped below its plan's ceiling from the dashboard, under the key's restrictions. That is worth doing for any key you have handed to someone else: it caps the blast radius of a runaway loop at a number you chose, instead of at your plan's.

The effective limit is the lowest of the plan ceiling, your own setting, and any cap support has applied to the account.

The x-rate-limit-limit header always reports the one actually in force.

Reading your usage from a response

Every successful response carries the current state of both ceilings, so you rarely need to ask for it separately:

HeaderMeaning
x-rate-limit-limitRequests per minute in force for this key
x-rate-limit-remainingRequests left in the current window
x-rate-limit-resetSeconds until the window refills — sent on 429
retry-afterSeconds to wait before retrying — sent on 429
x-api-credits-usedCredits spent this billing cycle
x-api-remaining-creditsCredits left this cycle
x-api-max-creditsThe cycle's allowance
x-api-renewalUnix timestamp when the allowance resets
x-api-current-planPlan the key is billed on
x-api-trialingPresent and true while a trial is running
bash
curl -sS -D - -o /dev/null \
  'https://api.apiverve.com/v1/dadjokes' \
  -H 'x-api-key: YOUR_API_KEY' | grep -i '^x-'
The rate-limit headers are invisible to browser JavaScript

Only the four credit headers are in the CORS exposed set. From a browser, x-rate-limit-limit and its siblings read as null even though they are on the wire. See CORS.

Which 429 is this

Read error. The two cases word themselves differently on purpose:

json
{
  "status": "error",
  "error": "Rate limit exceeded. Max 60 requests per minute.",
  "data": null,
  "premium": {
    "message": "Upgrade your plan for higher rate limits.",
    "upgrade_url": "https://dashboard.apiverve.com/plans"
  }
}

That one is transient. Wait retry-after seconds and repeat the identical request.

json
{
  "status": "error",
  "error": "Monthly credit limit reached. Credits will reset on your renewal date.",
  "data": null,
  "premium": {
    "message": "Need more credits? Upgrade your plan or contact support.",
    "upgrade_url": "https://dashboard.apiverve.com/plans"
  }
}

That one is not transient, and retrying it is the worst thing you can do — it burns your concurrency budget producing the same answer. Stop, and surface it to a human.

The programmatic test is the credit header, not the string:

js
if (res.status === 429) {
  const left = Number(res.headers.get('x-api-remaining-credits'));
  if (left === 0) throw new Error('Out of credits — top up or wait for renewal');
  await sleep(Number(res.headers.get('retry-after') || 10) * 1000);
  // ...then retry
}

Three wordings exist for the credit case, because the fix differs: a free account is told to upgrade, a trial is told the trial allowance is capped, and a paid account is told its renewal date. All three carry premium.upgrade_url.

Backing off correctly

Honour retry-after when it is present, and use exponential backoff with jitter when it is not:

js
async function call(url, key, tries = 4) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url, { headers: { 'x-api-key': key } });
    if (res.status !== 429) return res;

    if (Number(res.headers.get('x-api-remaining-credits')) === 0) {
      throw new Error('Out of credits');           // never retry this one
    }
    const wait = Number(res.headers.get('retry-after')) || 2 ** i;
    await new Promise((r) => setTimeout(r, wait * 1000 + Math.random() * 400));
  }
  throw new Error('Still rate limited after retries');
}

The jitter matters more than it looks. Without it, every worker that got limited at the same moment retries at the same moment, and the second wave trips the limiter exactly like the first.

Retrying aggressively makes it last longer

When a key is rate-limited, the edge remembers it and holds that key in a short cooldown — around 50 seconds. Hammering during the cooldown does not shorten it. Backing off properly usually gets you through in one wait; retrying in a tight loop can stay locked out for minutes.

Repeated identical failures

There is a third throttle that has nothing to do with volume. If the same call keeps failing the same way — a malformed parameter, a missing required input — that exact call is throttled and the original error is replayed back to you.

This is a guard against a loop retrying a permanently broken call forever, and it costs nothing to avoid: an input the source rejected has to change before it is worth sending again. See errors.

Staying under the ceiling

  • Cache what does not move. Country lookups, currency symbols and timezone data change on a scale of years. A cache in front of them removes the calls entirely, which is better than pacing them.
  • Use batch for lists. One batch call is one request against the rate limit, whatever it contains — the per-item cost is charged in credits, not in requests. This is the single biggest lever if you are limited rather than out of credits.
  • Give each workload its own key. A sub-key with its own limit means a backfill job cannot starve your live traffic. See sub-keys.
  • Queue, don't parallelise. Concurrency ceilings are low by design. A work queue with a fixed number of workers matched to your plan is more throughput than an unbounded Promise.all.

Next

Every other way a call can fail, and which are worth retrying, is in errors. For what a call costs against your allowance rather than your rate, see credits and billing.

Was this page helpful?

Last updated