Skip to content

Rate limiting

The ceilings a request passes, and how to keep batch work away from checkout.

A 429 is not a failure of the API. It is the API telling you that something in your code is issuing requests faster than it needs to, and the only reason it ever reaches a customer is that the noisy job and the checkout call share the same quota.

The quota table lives in Rate Limits. This page is about not needing it.

Four ceilings, checked in order

Every authenticated request passes four independent counters, each on a rolling 60 second window. The first one that is full answers, and the body names which.

typeCountsScope
teamEvery request your team makesYour plan's ceiling
api_keyEvery request made with one key100 per minute
ipEvery request from one source address120 per minute
endpointRequests touching payments, transfers, refunds or customersTighter, and scaled by plan

Two consequences are worth internalising. The per-key ceiling of 100 is lower than most plan ceilings, so splitting traffic across several keys genuinely raises your headroom, and running everything through one key genuinely caps it. And the endpoint ceilings are the ones a burst hits first: a loop over payments will trip endpoint long before it troubles team.

Requests that arrive without a recognised key are counted per IP at 30 per minute, which is what you see when a key is wrong rather than merely busy.

Read the ceiling, do not hardcode it

Every response carries your current team-level position. Those three headers are the truth, and they move when your plan does.

Headers on every response
X-RateLimit-Limit: 360
X-RateLimit-Remaining: 357
X-RateLimit-Reset: 1748083260

If a job of yours is anywhere near the limit, have it read X-RateLimit-Remaining and slow itself down before it gets pushed. Reacting to the header costs nothing; reacting to the 429 costs a round trip and a delay you did not choose.

Two shapes of 429

Most 429s come from the four counters above and carry a type. A few routes have a second, dedicated throttle on top, and those answer without one. POST /transfers is the one to know: it carries a flat ceiling of 20 per minute per team, independently of your plan, because a payout executes with no human in the loop.

The two bodies
{
  "code": 429,
  "status": "Too Many Requests",
  "message": "Too many requests",
  "type": "endpoint",
  "retry_after": 24,
  "retry_after_human": "00:00:24"
}

{
  "code": 429,
  "status": "Too Many Requests",
  "message": "Too Many requests. Merchant limit : 360",
  "retry_after": 24
}

Read type when it is there, and treat its absence as a route-specific throttle rather than as a malformed response. Both shapes carry retry_after in seconds, and both set the Retry-After header.

Wait the time you were given

The delay is in the response. Guessing produces either a wasted wait or a second 429.

Honour Retry-After, then back off
import { WajubRateLimitError } from '@wajub/node';

export async function withRateLimitRetry(fn, maxAttempts = 5) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (!(err instanceof WajubRateLimitError) || attempt >= maxAttempts) throw err;

      const seconds = err.retryAfter ?? 2 ** attempt;
      await new Promise((r) => setTimeout(r, seconds * 1000 + Math.random() * 500));
    }
  }
}

The SDK reads Retry-After for you and puts it on err.retryAfter. The random tail matters when several workers were throttled together: without it they all come back at the same instant and refill the bucket they were waiting on.

Keep batch work away from checkout

Reconciliation sweeps, settlement exports, and any loop over transfers or beneficiaries are where 429s are made. They are also the easiest to fix, because nobody is waiting on them.

HabitInstead
Promise.all over every idA pool of 5 to 10, see Performance
A sweep every minuteA sweep every 15 minutes, over a wider window
Running the export at noonRunning it when your checkout is quiet
One key for everythingA separate key for batch work, with its own 100 per minute

That last row is the cheapest win on the page. A restricted key scoped to the reads your job needs gets its own per-key counter, so a runaway export exhausts its own budget and leaves the key your checkout uses untouched.

When you genuinely need more

If the ceiling is limiting real traffic rather than a loop, the plan's max_requests is what moves. Contact support with the endpoint, the pattern, and the volume you expect. Raising a limit around a job that polls every payment every five seconds only delays the conversation.

What did you think of this content?