Rate Limiting
100 requests per minute per key, and how to back off cleanly
The Servicebay API enforces rate limits to keep the service responsive for
everyone. The main limit is applied per API key (or per OAuth
clientId + organisation), so multiple integrations sharing a credential
share one bucket.
Current limit: 100 requests per minute per API key and per OAuth grant. If you need more, email support@servicebay.io — higher limits are available for production integrations on request.
Production requires a distributed limiter
In production the API uses Upstash Redis. If that
backend is not configured, authenticated and unauthenticated calls
return 503 (Service temporarily unavailable.) rather than silently
falling back to a per-instance counter.
If Redis is configured but unreachable:
- Valid API-key and OAuth traffic fail open (the request is allowed) so a Redis blip does not take the API down.
- Failed-authentication and developer-portal throttles fall back to per-instance memory so brute-force protection stays on.
Local and preview deployments may use the in-memory limiter.
Other limits
- Failed authentication is throttled per IP address (10/min). Repeatedly
sending a missing, malformed, or invalid API key or Bearer token will
start returning
429from that IP. Fix the credential and the limit clears on its own. - Dashboard/management endpoints (API keys, OAuth clients, connected apps; authenticated with a Firebase ID token) are limited to 30/min per user account.
- The OAuth Worker applies its own limits:
/authorize30/min,/tokenand/revoke60/min, dynamic client registration 10/min and 100/day per IP.
Response headers
Every authenticated 2xx / 4xx / 5xx response includes the current
rate-limit state (once a credential has been accepted, or a failed-auth
budget has been charged):
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute |
X-RateLimit-Remaining | Remaining requests in the current window |
X-RateLimit-Reset | Unix timestamp when the limit resets |
Retry-After | Seconds until the window resets (set on 429) |
Example (429):
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705325400
Retry-After: 12When you hit the limit
A 429 Too Many Requests response is returned, with the same envelope as any
other error. Wait Retry-After seconds (or until X-RateLimit-Reset)
before retrying. A limiter outage is 503, not 429 — retry with
backoff, and quote requestId if the body includes one.
{
"success": false,
"error": "Rate limit exceeded. Please wait before making more requests."
}Retry pattern
A small wrapper that respects Retry-After / X-RateLimit-Reset and
retries on 429 and 503:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429 && response.status !== 503) return response;
const retryAfter = parseInt(
response.headers.get("Retry-After") ?? "0",
10
);
const resetUnix = parseInt(
response.headers.get("X-RateLimit-Reset") ?? "0",
10
);
const waitMs = Math.max(
250 * 2 ** attempt,
retryAfter ? retryAfter * 1000 : 0,
resetUnix ? resetUnix * 1000 - Date.now() : 0
);
if (attempt === maxRetries - 1) return response;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
throw new Error("Max retries exceeded");
}import time
import requests
def fetch_with_retry(method, url, max_retries=3, **kwargs):
for attempt in range(max_retries):
res = requests.request(method, url, **kwargs)
if res.status_code not in (429, 503):
return res
retry_after = int(res.headers.get("Retry-After", "0"))
reset_unix = int(res.headers.get("X-RateLimit-Reset", "0"))
wait_seconds = max(
0.25 * (2 ** attempt),
retry_after,
reset_unix - int(time.time()) if reset_unix else 0,
)
if attempt == max_retries - 1:
return res
time.sleep(wait_seconds)
raise RuntimeError("Max retries exceeded")Best practices
- Monitor
X-RateLimit-Remaining— pre-emptively slow down before you hit zero. - Use exponential backoff for retries beyond the first one.
- Cache read-mostly data to reduce repeat calls.
- Batch operations where possible — combine multiple writes into fewer round-trips.
- Long-poll responsibly — for "watch for changes" use cases, prefer polling at 30-second intervals over tight loops.