Rate limits

Two independent rate-limit layers guard the public API. Both must pass.

The public API applies two independent rate-limit layers. A request must pass every layer that applies — the strictest active layer wins. When exceeded, requests return 429 Too Many Requests with a Retry-After header (seconds) and body { "error": "rate_limited" }.

Layer 1 — per API-key token bucket

Enforced by authenticateSdkRequest for every authenticated SDK request (in-isolate token bucket). Resolution order:

  • Key override api_keys.rate_limit_per_minute, if set.
  • Otherwise the app default apps.sdk_rate_limit_per_minute, if set.
  • Otherwise the built-in fallback 300 requests / minute per API key.

Layer 2 — per (app, endpoint, scope)

Enforced by checkSdkRateLimit at each call site. Resolution order:

  • Row for (app_id, endpoint, scope) in app_rate_limits, if present.
  • Otherwise the wildcard row (app_id, endpoint='*', scope).
  • Otherwise app_settings.rate_limit_default_per_min — falls back to 600 requests / minute at the default scope (device).
  • If rate_limit_enabled=false the whole layer is off.

Layer 2 is only active on routes whose handler actually calls checkSdkRateLimit. Today those are:

  • /ads/list, /ads/event
  • /events
  • /flags/impressions
  • /forms/get, /forms/submit, /forms/upload-url
  • /inbox/list, /inbox/count, /inbox/ack, /inbox/read-all

All other authenticated routes (/register, /sync,/controls, /controls/delta, /controls/stream,/controls/events, /flags/ruleset) go through Layer 1 only.

Note
/session bypasses both layers on purpose — verified in src/routes/api/public/sdk.v1.session.ts. The kill-switch endpoint must remain reachable even when limits are misconfigured. It does not call checkSdkRateLimit, and authenticateSdkRequest is invoked in its "skip rate limit" mode.

Overrides

DashboardApps → your app → Rate limits

Add a row for the endpoint you need to loosen or tighten. Scope can be device, ip, api_key, or app. Because both layers run, tightening one does not loosen the other.

Client rules

  • Respect Retry-After — sleep at least that long before retrying.
  • Use exponential backoff with jitter after the initial wait.
  • Batch events (up to 200 per request) rather than one-at-a-time.
  • Poll no faster than every 30 seconds in foreground; less in background.
Back to top