Skip to content
  • Follow System
  • English
  • 中文

Rate Limits

Picora applies tiered rate limits per API Key (and per OAuth token) to keep the platform fair and the Workers Free Plan within budget. Rate limits are per-account, not per-IP — sharing a Key across machines doesn’t multiply the budget.

Tiers

TierEndpointsLimitBurst
readGET list/detail, /v1/auth/verify, /v1/user/me/*600 / minute30
uploadPOST /v1/{images,videos,audio,docs}60 / minute5
mutationPATCH / DELETE (incl. batch)120 / minute10

Plus a 5000 requests / hour global ceiling per Key. Admin users get a ×10 multiplier.

Response headers

Every successful API response carries the rate-limit signal in two header families so you can pick whichever your client library understands:

  • RFC 9239 form (recommended for new clients):
    • RateLimit-Limit — total budget for the current window
    • RateLimit-Remaining — what’s left
    • RateLimit-Reset — Unix seconds until the window resets
  • Legacy GitHub-style (Scalar Try-it, older OpenAPI tools):
    • X-RateLimit-Limit
    • X-RateLimit-Remaining
    • X-RateLimit-Reset

Both expose the same numbers; pick whichever your stack already understands. Browsers can read both via the CORS Access-Control-Expose-Headers allowlist.

When you hit the limit

HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{
"success": false,
"error": "Rate limit exceeded",
"code": "RATE_LIMITED",
"meta": { "retryAfterSec": 12 }
}

Retry-After is always set to the integer number of seconds you should wait before retrying.

Backoff strategy

The official SDK does this for you (retryOnRateLimit: true, default). If you build your own client, follow these rules:

  1. On the first 429, wait Retry-After seconds (or 1 s if missing). Retry once.
  2. If a second 429 arrives, double the wait. Cap at 4 s.
  3. After three failed retries, surface the error to the user — don’t loop forever.
async function withBackoff<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
try {
return await fn()
} catch (err) {
if (!(err instanceof Response) || err.status !== 429 || attempt === maxAttempts - 1) throw err
const retryAfter = Number(err.headers.get('Retry-After') ?? 0)
const wait = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 1000
await new Promise((r) => setTimeout(r, wait))
}
}
throw new Error('unreachable')
}

Header coverage CI

Picora runs a CI gate (apps/api/scripts/audit-rate-limit-coverage.ts) that fails any change to openapi.json if a non-exempt endpoint forgets to declare the RateLimit-* header triplet. Exempt endpoints are:

  • /health
  • /openapi.json, /openapi-public.json, /openapi-internal.json
  • /.well-known/**
  • /webhooks/** (signature-protected, not throttled)

If you spot a public endpoint that should expose the headers but doesn’t, please file a bug — the audit script catches new omissions but the legacy spec is being remediated incrementally.