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

# Rate limiting

> OpenFX exposes IETF-standard RateLimit-* headers on every response. LIVE: 300 requests / 10s. SANDBOX: 600 requests / 10s. On a 429 or 409 IDEMPOTENCY_IN_FLIGHT, wait Retry-After seconds.

Every response carries IETF-standard `RateLimit-*` headers so you know your cap and when the current window resets. The limit is per org, per environment (`LIVE` or `SANDBOX`) — **300 requests / 10-second window on LIVE**, **600 requests / 10-second window on SANDBOX** (2× LIVE). 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                                       |
| `RateLimit-Remaining` | integer           | Requests left in the current window. **Not yet emitted** — pending an edge-layer rollout; absent until then |

```http theme={null}
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
      IDEMPOTENCY_IN_FLIGHT`, prefer the `Retry-After` header (also seconds) — the
  more specific, purpose-built signal for exactly when to retry.
</Note>

## Default limits

| Environment | Limit                                     |
| ----------- | ----------------------------------------- |
| `LIVE`      | 300 requests / 10-second window           |
| `SANDBOX`   | 600 requests / 10-second window (2× LIVE) |

The limit is per org, per environment — bucketed by whichever `X-App-Mode` (`LIVE` or `SANDBOX`) the request used, shared across every API key under that org. Regional enforcement means counters are tracked per geographic edge location, so a distributed source may see a multiplied effective limit.

## 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 AUTH_TOKEN_EXPIRED` (or a
  `401` `AUTH_TOKEN_INVALID` with `details.reason: SIGNATURE_INVALID` if you
  reused the signature). The pattern below takes a `signRequest` function that
  mints both on each attempt.
</Warning>

```javascript theme={null}
// `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. The rate-limit posture lives in the headers, so `details` is empty:

```json theme={null}
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "type": "RATE_LIMITED",
    "message": "Too many requests. Try again after the window resets.",
    "retryStrategy": "WAIT_THEN_RETRY_SAME_KEY",
    "details": {}
  }
}
```

Headers on a 429:

```http theme={null}
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 IDEMPOTENCY_IN_FLIGHT` too — see [Idempotency](/v3/idempotency).

## Need a higher rate limit?

Email [support@openfx.com](mailto:support@openfx.com). Provide your org ID, whether you need it on `LIVE` or `SANDBOX`, 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 at the rate-limit floor.** A 300 req/10s (LIVE) ceiling and a 1-second poll across 30 background workers leaves zero headroom for actual user-driven requests. Pace well under the limit.
* **Mixing Live and Sandbox traffic against the same limit assumptions.** SANDBOX's limit is 2× LIVE's, and they're separate buckets — verify your limits in each.

## What's next

<CardGroup cols={2}>
  <Card title="Metadata & tracing" icon="fingerprint" href="/v3/metadata-and-tracing">
    X-Trace-Id on every response.
  </Card>

  <Card title="Idempotency" icon="repeat" href="/v3/idempotency">
    In-flight requests and safe retries.
  </Card>
</CardGroup>
