Skip to main content

Rate limits

The Orunbase API rate-limits every request with a token bucket that refills continuously over a 60-second window. Two independent scopes are evaluated per request, and either one can deny it:

  • org — keyed on the workspace in the path (/v1/organizations/{ref}/…); skipped on routes with no workspace segment.
  • identity — keyed on a SHA-256 fingerprint of the bearer token (the same token shares one bucket regardless of actor type). Unauthenticated requests fall back to an anonymous bucket keyed by client IP + route family, so a single IP cannot bypass the limit.

Because buckets refill continuously (limit ÷ 60 tokens per second), short bursts up to the full limit are absorbed and capacity comes back gradually rather than at a hard window boundary.

Limits by route family

Every route belongs to a family; each family has its own caps per 60 s window:

FamilyRoutesPer identityPer workspace
authLogin, OAuth, CLI device flow, OIDC exchange1060
auditAudit log reads120600
orgWorkspaces, members, invitations, teams, API keys60300
projectProjects and environments60300
configSettings, feature flags, secrets60300
webhooksWebhook endpoints and deliveries60300
meteringUsage and quotas60300
billingPlans, checkout, portal60300
notificationsNotification preferences60300
integrationsGitHub integration60300
stateState plane60300

auth is deliberately tight — login flows are the brute-force target. audit is read-only and raised so export loops do not starve.

Response headers

Every response (not just 429s) carries per-scope counters:

HeaderMeaning
X-RateLimit-Limit-org / X-RateLimit-Limit-identityBucket capacity for the scope
X-RateLimit-Remaining-org / X-RateLimit-Remaining-identityWhole tokens left
X-RateLimit-Reset-org / X-RateLimit-Reset-identityUnix epoch seconds when the bucket is full again

The -org headers appear only on workspace-scoped routes.

429 responses

When a bucket is empty, the API returns 429 with a Retry-After header (seconds) and the standard error envelope naming the scope that tripped:

{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded for identity scope. Retry after 3 seconds.",
"details": { "scope": "identity", "retryAfterSeconds": 3 },
"requestId": "req_2b3c4d5e6f7a8b9c0d1e2f3a"
}
}

Enforcement precision

Enforcement differs by method class, trading precision for latency where it is safe:

  • Writes (POST, PATCH, PUT, DELETE) use durable, globally consistent counters — each bucket is a single-threaded Durable Object, so the consume is atomic and the cap is exact.
  • Reads (GET, HEAD) use a fast local approximation with no network round-trip. The effective global read limit can exceed the nominal cap under wide fan-out; caps are generous and reads are not the abuse vector writes are.
note

The limiter fails open: if its backing store is unavailable, requests are admitted (without rate-limit headers) rather than rejected. An infrastructure outage in the limiter never turns into a platform-wide 429.

Backing off

  • Honor Retry-After (or details.retryAfterSeconds) on 429 before retrying; add jitter when many workers share an identity or workspace.
  • Watch X-RateLimit-Remaining-* proactively and smooth bursts before you hit zero.
  • Spread bulk work: buckets refill continuously, so a steady ~1 req/s per identity on default families never trips.
  • Pair retried writes with an idempotency key so a request denied mid-burst is safe to resend.

The SDK throws a typed RateLimitError with everything decoded — it has no built-in retries, so drive the backoff yourself:

import { RateLimitError } from "@saas/sdk";

try {
await client.projects.create(orgId, body, { idempotencyKey: key });
} catch (err) {
if (err instanceof RateLimitError) {
console.log(err.scope); // "org" | "identity" | null
console.log(err.retryAfterSeconds); // number | null
console.log(err.identityWindow); // { scope, limit, remaining, resetAt } | undefined
await sleep((err.retryAfterSeconds ?? 1) * 1000);
// retry with the same idempotency key
}
}