Rate limits
How the public bichito endpoints throttle requests, and what your client should do when it gets a 429.
The public surface of the bichito API is rate-limited so a single bad actor (or a buggy client) can't bring everything down. The limits are intentionally generous for legitimate traffic and tight enough to discourage abuse.
The limits
| Endpoint | Limit | Bucket | Why |
|---|---|---|---|
POST /api/v1/auth/signup | 5 / hour | client IP | Stops automated mass-registration. |
POST /api/v1/auth/login | 10 / minute | client IP | Anti-spray from a single host. |
POST /api/v1/auth/login | 20 / hour | email submitted | Anti credential-stuffing against a specific account, even from rotating IPs. |
POST /api/v1/bugs | 60 / minute | X-API-Key value | Caps damage from a leaked widget key. |
Both login limits apply at the same time — the stricter one wins. So a single host that knows a real email can try at most 10 passwords/minute against it, and after 20 attempts in an hour the account is frozen for that hour regardless of where the attempts came from.
What clients see when limited
When a request is denied, the API returns:
- HTTP
429 Too Many Requests - A JSON body explaining which limit fired:
{
"detail": "Rate limit exceeded: 10 per 1 minute"
}
- A
Retry-Afterheader with the number of seconds the client should wait before retrying:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
content-type: application/json
Handling 429 in your client
A robust client should:
- Stop hammering. Don't retry immediately.
- Read
Retry-Afterand back off for at least that many seconds. Apply jitter so clients don't all retry at the same instant. - Surface a clear message to the user — for the widget, "we received too many reports lately, please try again in a minute" is fine.
Pseudocode:
async function postWithBackoff(url: string, body: unknown) {
const res = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
if (res.status === 429) {
const retry = Number(res.headers.get("retry-after") ?? "60");
const jitter = Math.random() * 1000;
await new Promise((r) => setTimeout(r, retry * 1000 + jitter));
return postWithBackoff(url, body);
}
return res;
}
Need higher limits?
The widget limit (60 reports / minute / API key) covers anything a normal app will ever produce. If you have a legitimate reason to exceed it — a high-traffic incident response tool, a public widget reused across many sites under one key — get in touch and we'll raise the cap on a per-account basis.
Implementation notes
- Backed by
slowapiwith the in-memory storage by default. When the API is deployed across multiple workers we switch to a Redis URI via theRATE_LIMIT_STORAGE_URIenvironment variable; clients don't notice the change. - Per-IP buckets honor the first hop of
X-Forwarded-Forwhen the API runs behind a proxy, so the limit is keyed off the real client and not the proxy. - Quota limits (the 100-reports/month free plan cap) are separate from rate limits; you can hit either independently.