Docs

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 429 from 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: /authorize 30/min, /token and /revoke 60/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):

HeaderDescription
X-RateLimit-LimitMaximum requests per minute
X-RateLimit-RemainingRemaining requests in the current window
X-RateLimit-ResetUnix timestamp when the limit resets
Retry-AfterSeconds until the window resets (set on 429)

Example (429):

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705325400
Retry-After: 12

When 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

  1. Monitor X-RateLimit-Remaining — pre-emptively slow down before you hit zero.
  2. Use exponential backoff for retries beyond the first one.
  3. Cache read-mostly data to reduce repeat calls.
  4. Batch operations where possible — combine multiple writes into fewer round-trips.
  5. Long-poll responsibly — for "watch for changes" use cases, prefer polling at 30-second intervals over tight loops.