Predictefy Docs
Browse documentation

Rate limits & retries

Per-plan request windows, what a 429 actually means, and how to back off correctly.

Every API key is rate limited per plan. Exceeding the window returns 429 RATE_LIMITED with retryable: true in the standard error envelope.

Rate limiting and credits are separate controls. The limiter caps how fast you may call; credits cap how much you may call in total. A request can pass the limiter and still fail with 402 INSUFFICIENT_CREDITS, or fail the limiter without ever being charged.

Limits by plan

PlanRequestsAPI keysConcurrent WebSocket streams
Free60 / min12
Builder300 / min320
Pro3,000 / min10100
Scale10,000 / min25500
Enterprisenegotiatednegotiatednegotiated

The window is a rolling 60 seconds per API key, not per account and not per endpoint. Two keys on one account each get the full allowance; one key spread across ten processes shares a single allowance.

Enterprise plans and individual keys can carry a bespoke override. An override is applied as a per-second window rather than per-minute — so a key provisioned at 50/s is allowed 50 in any given second, not 3,000 spread freely across a minute.

What the response tells you

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "rate limit exceeded — retry shortly",
    "retryable": true
  }
}

Backing off

With no server-supplied delay, use exponential backoff with jitter. The window is 60 seconds, so a client retrying every second spends its next allowance on failures.

async function withRetry<T>(call: () => Promise<T>, attempts = 5): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await call();
    } catch (err) {
      const code = (err as { code?: string }).code;
      // Only these are worth retrying. A 400 or 401 will fail identically forever.
      if (code !== 'RATE_LIMITED' && code !== 'PLATFORM_UNAVAILABLE') throw err;
      if (attempt >= attempts - 1) throw err;
      // 1s, 2s, 4s, 8s … plus jitter so parallel workers do not resynchronise.
      const backoff = 2 ** attempt * 1000 + Math.random() * 1000;
      await new Promise((r) => setTimeout(r, backoff));
    }
  }
}

The TypeScript SDK retries GET requests once on 429 by default (retryOn429). Writes are never auto-retried — a submit or cancel that may have reached the venue must not be replayed by a client library. For those, retry deliberately and send an Idempotency-Key; see Trading & execution.

Which errors to retry

CodeHTTPRetry?
RATE_LIMITED429Yes — back off, then retry
PLATFORM_UNAVAILABLE503Yes — back off, then retry
CATALOG_UNAVAILABLE / HISTORY_UNAVAILABLE503Yes — back off, then retry
INSUFFICIENT_CREDITS402No — retrying cannot succeed until the balance changes
VALIDATION_ERROR400No — fix the request
UNAUTHORIZED401No — fix the key
NOT_SUPPORTED400 / 501No — an honest capability gap, not a failure

retryable is present on every error and is the field to branch on. Treat it as authoritative over the HTTP status.

Staying under the limit

  • Prefer cursors over parallel offset pages. Following nextCursor keeps one request in flight; twenty parallel offset pages spend twenty of the allowance in one second.
  • Batch where a batch verb exists. fetchOrderBooks takes many outcomes in one request. Note it is priced by items, so it saves allowance rather than credits.
  • Stream instead of polling. A WebSocket subscription delivers book and trade updates without consuming the request window at all. Polling a book every second on Free spends the entire minute allowance on one market.
  • Cache what does not move. Venue capability maps (has) and taxonomy (fetchCategories, fetchTags) change rarely; re-fetching them per request is pure overhead.
  • Spread scheduled work. Offset cron jobs by a random delay so batch runs do not collide with each other or with interactive traffic.

When rate limiting itself is degraded

If the limiter's backing store is unreachable, the API fails open for reads — requests are served rather than rejected, and credits remain the spend backstop. Side-effecting routes fail closed with 503 PLATFORM_UNAVAILABLE instead, because replaying an uncertain write is worse than refusing it.

No special handling is required. It is documented so that a burst of 503s on writes while reads continue is recognisable as designed behaviour rather than a partial outage.