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.
type | Counts | Scope |
|---|---|---|
team | Every request your team makes | Your plan's ceiling |
api_key | Every request made with one key | 100 per minute |
ip | Every request from one source address | 120 per minute |
endpoint | Requests touching payments, transfers, refunds or customers | Tighter, 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.
X-RateLimit-Limit: 360
X-RateLimit-Remaining: 357
X-RateLimit-Reset: 1748083260If 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.
{
"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.
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.
Cap the attempts
A retry loop with no ceiling turns a ten minute incident into a queue full of jobs that have each been retrying for ten minutes. Stop after a handful, fail the job, and let your alerting see it.
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.
| Habit | Instead |
|---|---|
Promise.all over every id | A pool of 5 to 10, see Performance |
| A sweep every minute | A sweep every 15 minutes, over a wider window |
| Running the export at noon | Running it when your checkout is quiet |
| One key for everything | A 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.