Rate limits
Farbeam limits requests in units per minute. Each endpoint has a cost in units.
| Caller | Limit | Bucket |
|---|---|---|
| No key | 30 units per minute | One bucket per network, keyed by a SHA-256 hash of the client IP |
| Free key | 120 units per minute, and 20,000 units per UTC day | One bucket per key |
#Unit costs
| Endpoint | Cost |
|---|---|
GET /wallets/{address}, GET /wallets/{address}/activity, GET /tokens/{chain}/{address} | 3 |
GET /tx/{chain}/{hash} | 2 |
GET /health, GET /status, GET /me, DELETE /me | 0 |
GET /auth/session, POST /auth/logout, GET /account | 0 |
POST /keys, DELETE /keys/{id}, POST /account/telegram, DELETE /account/telegram | 0 |
| Every other endpoint | 1 |
The live stream has no unit cost. It has its own limit of 6 open streams per network and 400 in total.
#How the window works
Windows are fixed. Each one starts on a whole UTC minute and resets to zero on the next one. A client that spends its limit in the last second of one minute can spend it again in the first second of the next.
The counter lives in process memory, so it also resets when the service restarts. Three details matter when you write a client:
- The cost is added before the check. A request that goes over still adds to the counter for that window.
- A request that fails validation, such as a bad address, still costs its units. It has already passed the rate check.
- A request for an unknown path, a wrong method or a bad key costs nothing and returns no rate headers.
#Headers
Every response from a metered endpoint carries three headers. Endpoints with a cost of 0 do not send them.
| Header | Meaning | Example |
|---|---|---|
x-ratelimit-limit | Units allowed per minute for this caller | 120 |
x-ratelimit-remaining | Units left in this window. Never below 0. | 118 |
x-ratelimit-reset | Unix time in seconds when the next window starts | 1793820240 |
Retry-After | Seconds to wait. Sent with a unit-limit 429. | 56 |
All of them are readable from browser code. The API sets access-control-expose-headers.
#A real 429
This response was captured on 2026-11-04 UTC by spending a test key's limit.
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 120
x-ratelimit-remaining: 0
x-ratelimit-reset: 1793820240
retry-after: 56{
"error": {
"code": "rate_limited",
"message": "Limit is 120 units per minute.",
"retry_after": 12,
"request_id": "req_53a105fabbcd"
}
}The body repeats the wait in retry_after, in seconds.
Two other caps also return 429 rate_limited, and neither sends Retry-After. They are the sign-in challenge cap (30 per hour per network) and the test delivery cap (10 per rule per hour). Waiting one minute does not clear them, so do not retry them in a loop.
#Daily limit
Each key can spend 20,000 units per UTC day. Above that, requests to endpoints with a cost return 429 daily_limit until 00:00 UTC, with no Retry-After. Requests without a key have no daily limit. Endpoints with a cost of 0 are not blocked. usage[].calls from Current key shows the count, and it adds 1 for a request whose cost is 0.
#Backoff code
Wait for Retry-After when it is present. For 502 and network errors, use a capped exponential delay with jitter. Do not retry other 4xx codes.
# Retries a GET up to 5 times. Prints the body on success.
url="https://farbeamlabs.com/api/v1/transfers?limit=5"
for attempt in 1 2 3 4 5; do
code=$(curl -s -o /tmp/farbeam.json -D /tmp/farbeam.hdr -w "%{http_code}" "$url")
if [ "$code" = "200" ]; then cat /tmp/farbeam.json; exit 0; fi
if [ "$code" != "429" ] && [ "$code" != "502" ] && [ "$code" != "000" ]; then cat /tmp/farbeam.json; exit 1; fi
wait=$(grep -i '^retry-after:' /tmp/farbeam.hdr | tr -dc '0-9')
sleep "${wait:-$((2 ** attempt))}"
done
exit 1export async function getWithBackoff(url, init = {}, tries = 5) {
let lastError;
for (let attempt = 0; attempt < tries; attempt++) {
let wait;
try {
const res = await fetch(url, init);
if (res.status !== 429 && res.status !== 502) return res;
const after = Number(res.headers.get("retry-after"));
lastError = new Error(`HTTP ${res.status}`);
wait = after > 0 ? after * 1000 : null;
} catch (err) {
lastError = err;
}
wait ??= Math.min(30000, 1000 * 2 ** attempt) * (0.5 + Math.random() / 2);
await new Promise((resolve) => setTimeout(resolve, wait));
}
throw lastError;
}import random, time
import requests
def get_with_backoff(url, tries=5, **kwargs):
last_error = None
for attempt in range(tries):
wait = None
try:
res = requests.get(url, timeout=30, **kwargs)
if res.status_code not in (429, 502):
return res
last_error = RuntimeError(f"HTTP {res.status_code}")
after = res.headers.get("retry-after")
wait = int(after) if after else None
except requests.RequestException as err:
last_error = err
if wait is None:
wait = min(30, 2 ** attempt) * (0.5 + random.random() / 2)
time.sleep(wait)
raise last_error#Spend units well
- Cache token reports and wallet reads on your side. The service caches them too, for the times listed here, so polling faster than that returns the same data and still costs units.
- Use
/transferswithmin_usdandchainfilters rather than filtering pages on the client. - Use the live stream or a webhook rather than polling
/transfers. - Check
/status, which costs 0 units, before a batch job.
There is no paid tier and no way to buy a higher limit today. Paid plans are Planned. See limits for every limit in one table.