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)inapp_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=falsethe 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.
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
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.