> ## Documentation Index
> Fetch the complete documentation index at: https://public-openfx.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limiting

> OpenFX exposes IETF-standard RateLimit-* headers on every response so you always know your cap and when it resets.

Every response carries IETF-standard `RateLimit-*` headers so you know your cap and when the current window resets. The limit is **300 requests / 10-second window**, tracked as a separate bucket per org, per environment (<span class="ofx-enum">LIVE</span> or <span class="ofx-enum">SANDBOX</span>), with the same cap in both. All API keys under an org share the same environment bucket; creating more keys does not raise the limit. When you exceed it you get a `429`; wait `Retry-After` seconds, then retry.

## Rate-limit headers

| Header | Type | Purpose |
| - | - | - |
| `RateLimit-Limit` | integer | Max requests allowed in the current window |
| `RateLimit-Reset` | integer (seconds) | Duration until the current window resets, **not** an epoch timestamp |

```http theme={"theme":{"light":"poimandres","dark":"poimandres"}}
HTTP/1.1 200 OK
RateLimit-Limit: 300
RateLimit-Reset: 8
```

<Note>
  **`RateLimit-Reset` is a duration, not a timestamp.** `RateLimit-Reset: 8`
  means "the window resets in 8 seconds from now." Compute your wait as
  `RateLimit-Reset * 1000` milliseconds from when you received the response, not
  by comparing it against a clock. On a `429` or a `409` <span class="ofx-enum">IDEMPOTENCY\_IN\_FLIGHT</span>,
  prefer the `Retry-After` header (also seconds), the more specific,
  purpose-built signal for exactly when to retry.
</Note>

## Default limits

| Environment | Limit |
| - | - |
| <span class="ofx-enum">LIVE</span> | 300 requests / 10-second window |
| <span class="ofx-enum">SANDBOX</span> | 300 requests / 10-second window |

The limit is per org, per environment: bucketed by whichever `X-App-Mode` (<span class="ofx-enum">LIVE</span> or <span class="ofx-enum">SANDBOX</span>) the request used, shared across every API key under that org. <span class="ofx-enum">LIVE</span> and <span class="ofx-enum">SANDBOX</span> are separate buckets with the same cap; usage in one doesn't count against the other.

## Handling rate limits: pattern

<Warning>
  **Mint a fresh JWT *and* a fresh `X-Request-Signature` per attempt: do not
  capture once.** JWTs are 60-second TTL and the signature is bound to the JWT's
  `nonce` ([Authentication](/v3/authentication)). Retries across several
  attempts can add up to more than 60s of wall-clock time. A captured JWT will
  expire mid-loop and your next request returns `401` <span class="ofx-enum">AUTH\_TOKEN\_EXPIRED</span> (or a
  `401` <span class="ofx-enum">AUTH\_TOKEN\_INVALID</span> with `details.reason: request_signature_invalid` if you
  reused the signature). The pattern below takes a `signRequest` function that
  mints both on each attempt.
</Warning>

```javascript theme={"theme":{"light":"poimandres","dark":"poimandres"}}
// `signRequest({ method, url, body })` returns { headers, body } with a freshly minted JWT
// AND a freshly computed X-Request-Signature each call. See the Authentication guide.
async function callWithBackoff(
  { method, url, body },
  signRequest,
  maxRetries = 5,
) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    // Mint per attempt: JWT TTL is 60s; backoff loops can exceed that, and the
    // signature is bound to the JWT's nonce so capturing once is doubly broken.
    const signed = signRequest({ method, url, body });
    const res = await fetch(url, {
      method,
      headers: signed.headers,
      body: signed.body,
    });

    if (res.status !== 429) return res;

    // Rate limited: Retry-After and RateLimit-Reset are both a number of
    // seconds to wait, NOT an epoch timestamp. Number(null) is 0 (which
    // passes isFinite!), so check for a missing header explicitly rather
    // than letting Number() coerce it. Otherwise, a response with neither
    // header retries immediately instead of falling back to backoff.
    const waitHeader =
      res.headers.get("retry-after") ?? res.headers.get("ratelimit-reset");
    const wait = waitHeader === null ? NaN : Number(waitHeader);
    const waitMs = Number.isFinite(wait) ? wait * 1000 : 1000 * (attempt + 1); // fall back to linear backoff
    await sleep(waitMs);
  }
  throw new Error("Rate limit retry budget exhausted");
}
```

## When you hit the limit

A 429 response uses the standard error envelope and includes a `Retry-After` header. Its `details` object is empty:

```json theme={"theme":{"light":"poimandres","dark":"poimandres"}}
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "type": "RATE_LIMITED",
    "message": "Rate limit exceeded. Retry after the indicated wait.",
    "retryStrategy": "WAIT_THEN_RETRY_SAME_KEY",
    "details": {}
  }
}
```

Headers on a 429:

```http theme={"theme":{"light":"poimandres","dark":"poimandres"}}
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 300
RateLimit-Reset: 8
Retry-After: 8
X-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736
X-Request-Timestamp: 2026-05-29T12:34:56.789Z
```

Compute your wait as `Retry-After * 1000` milliseconds (both `Retry-After` and `RateLimit-Reset` are a **duration** in seconds, not an epoch timestamp; prefer `Retry-After`, the more specific signal for exactly when to retry). The same `Retry-After` header is present on `409` <span class="ofx-enum">IDEMPOTENCY\_IN\_FLIGHT</span> too. See [Idempotency](/v3/idempotency).

## Need a higher rate limit?

Email [support@mail.openfx.com](mailto:support@mail.openfx.com). Provide your org ID, whether you need it on <span class="ofx-enum">LIVE</span> or <span class="ofx-enum">SANDBOX</span>, and a sense of the burst pattern you need.

## Common mistakes

* **Sleeping a fixed duration on 429 instead of waiting `Retry-After` seconds.** A hard-coded sleep either thrashes the limit or wastes throughput. Compute the wait from `Retry-After` (or `RateLimit-Reset` as a fallback).
* **Treating `RateLimit-Reset` as an epoch timestamp.** It's a duration in seconds from the response, not a Unix timestamp: `RateLimit-Reset * 1000 - Date.now()` computes a nonsensical wait. Use `RateLimit-Reset * 1000` directly.
* **Retrying a 429 indefinitely.** A single reset-wait is not a license to retry forever. Cap retry attempts; on exhaustion, surface the error to the caller.
* **Capturing the JWT or signature outside the retry loop.** JWTs are 60s TTL and the `X-Request-Signature` is bound to the JWT's `nonce`; both must be re-minted per attempt. Pass a `signRequest` function so each attempt gets a fresh token AND a fresh signature. See the Warning above.
* **Polling near the rate-limit ceiling.** A 300 req/10s ceiling and a 1-second poll across 30 background workers leaves zero headroom for actual user-driven requests. Pace well under the limit.
* **Assuming a support-granted limit increase applies to both environments.** <span class="ofx-enum">LIVE</span> and <span class="ofx-enum">SANDBOX</span> are separate buckets; a higher ceiling granted on one doesn't carry over to the other.

## What's next

<CardGroup cols={2}>
  <Card title="Metadata & tracing" icon="https://public-assets.openfx.com/docs/icons/metadata-tracing.svg" href="/v3/metadata-and-tracing">
    X-Trace-Id on every response.
  </Card>

  <Card title="Idempotency" icon="https://public-assets.openfx.com/docs/icons/idempotency.svg" href="/v3/idempotency">
    In-flight requests and safe retries.
  </Card>
</CardGroup>
