Rate limits
Limits are per key. Read the RateLimit headers, and on 429 wait for Retry-After.
Each key has its own limit. One busy key never slows down another key in the same workspace.
The limits
| Key | Requests per second | Burst |
|---|---|---|
bfl_live_ and bfl_test_ keys | 20 | 60 |
| MCP clients connected with OAuth | 10 | 30 |
The limit works like a bucket that holds up to the burst and refills at the steady rate. A key that has been idle can send the whole burst at once, then settles to the per second rate.
Who am I returns your key's limit in rateLimit, so your code can read it instead of hard coding it.
Headers
Every response carries the key's current budget.
| Header | Meaning |
|---|---|
RateLimit-Limit | The most requests the key can make in a burst. |
RateLimit-Remaining | Requests left right now. |
RateLimit-Reset | Seconds until the bucket is full again. |
When you hit the limit
Over the limit, the request fails with 429 rate_limited and a Retry-After header in seconds. Wait that long, then retry. Nothing ran, so retrying is safe for every route.
async function call(url: string, init: RequestInit = {}, tries = 3): Promise<Response> {
const res = await fetch(url, init);
if (res.status === 429 && tries > 1) {
const wait = Number(res.headers.get("Retry-After") ?? "1");
await new Promise((r) => setTimeout(r, wait * 1000));
return call(url, init, tries - 1);
}
return res;
}Staying under it
- Page with
limit=100instead of many small pages. - Cache what does not change often, such as the account list and fan lists.
- Prefer webhooks to polling.
message.receivedandtransaction.createdtell you when there is something new. - Use
fresh=trueonly when you need a live answer. Live reads are slower, so a loop of them holds your budget longer. - Run large jobs with a small, fixed number of requests in flight instead of firing them all at once.