Skip to navigation

Rate limits

Every API key on your account shares two limits:

  • A request rate. It works like a token bucket: you can send your plan’s burst all at once, and it refills at your plan’s sustained rate.
  • A monthly allowance. This is the total number of requests your plan includes for each billing period.

Only successful responses (2xx and 304) count against the allowance. Errors, including 403, 404 and 429, are free.

When you hit a limit

Both limits answer with 429 Too Many Requests, a Retry-After header in seconds, and no data. The problem code says which limit you hit:

codeWhat happenedWhat to do
rate_limitedYou sent requests faster than your plan’s rate.Wait Retry-After seconds, then slow down.
quota_exhaustedYou’ve used this period’s allowance. The response includes resets_at.Wait for the period to reset, or upgrade your plan.

If the API briefly can’t check your account or its limits, it returns 503 with code unavailable rather than serving the request unmetered. Wait Retry-After seconds and try again.

Usage headers

Every response that counts toward your limits carries two headers, including errors and 429s. They follow the IETF RateLimit header fields draft.

RateLimit-Policy: "allowance";q=500000;w=2592000
RateLimit: "allowance";r=499120;t=1723456
HeaderFieldMeaning
RateLimit-PolicyqRequests your plan allows per period.
wLength of the current period, in seconds.
RateLimitrRequests left this period, after this response.
tWhole seconds, rounded up, until the period resets and r returns to q.

Some things to keep in mind:

  • r is a snapshot. A response that doesn’t count, such as an error, reports r unchanged. Your other keys draw from the same allowance, so the next response can report less than you expect.
  • Retry-After wins. On quota_exhausted, r is 0 and t equals Retry-After. On rate_limited, r can still be positive, because the rate limit isn’t reported in these headers. Follow Retry-After either way.
  • Some responses don’t carry the headers. A 401, a 403 for an account without active access, a 503, or a request from a plan without REST access is rejected before it’s counted.
  • Parse the headers leniently. They’re Structured Field lists. Ignore policies and parameters you don’t recognize, because more may be added.

Reading the headers

const res = await fetch("https://api.statpad.dev/v1/leagues", {
headers: { Authorization: `Bearer ${process.env.STATPAD_API_KEY}` },
});
if (res.status === 429 || res.status === 503) {
const wait = Number(res.headers.get("Retry-After"));
// Wait `wait` seconds before the next request.
}
const left = res.headers.get("RateLimit")?.match(/;r=(\d+)/)?.[1];
if (left !== undefined) console.log(`${left} requests left this period`);