# Rate limits

> Per-account request limits per minute, the active-job cap, the rate-limit headers and how to back off.

Source: https://www.framequery.com/docs/rate-limits (updated 2026-10-13)

## Limits

Limits apply per account: every key of the account draws from the same buckets. Windows are one minute.

| Bucket | Limit | Counts |
|---|---:|---|
| Reads | 600 / minute | Every `GET` with a key: jobs, results, quota, delivery log |
| Job creation | 30 / minute | `POST /jobs`, `POST /jobs/from-url`, `POST /jobs/batch` (one request, whatever its clip count) |
| Other writes | 120 / minute | Everything else, for example `POST /jobs/{jobId}/cancel` |
| Keyless discovery | 120 / minute per client IP | `GET /openapi.json`, `/pricing`, `/pricing/estimate`, `/formats` |
| Webhook test | 5 / minute | `POST /webhooks/test` |

Discovery responses are also cacheable (`Cache-Control: public, max-age=300`), so cache them rather than calling them per job.

## Active jobs

Separately from request rates, an account can have a limited number of unfinished jobs at once (50 by default). Creating more returns `429` [`TOO_MANY_ACTIVE_JOBS`](https://www.framequery.com/docs/errors#TOO_MANY_ACTIVE_JOBS) with `Retry-After: 30` and a body that includes `maxActiveJobs` and `activeJobs`. Each clip of an independent batch counts as one job. If your workload needs more concurrency, [contact us](https://www.framequery.com/contact).

## Headers

Every response to a request that passed key authentication (success or error), and every discovery response, carries:

| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | Requests allowed per window in the bucket this request was charged to |
| `X-RateLimit-Remaining` | Requests left in the current window |
| `X-RateLimit-Reset` | When the window resets, as a **Unix timestamp in seconds** (not a number of seconds to wait) |
| `Retry-After` | On `429`: seconds to wait before retrying |

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1760356800
```

## Backing off

On `429` (and `503`), wait exactly `Retry-After` seconds and retry the same request, with the same `Idempotency-Key` for creates. Without a `Retry-After` (a `500`), use exponential backoff with jitter.

```python
import random, time
import httpx

def request_with_retries(client: httpx.Client, method: str, url: str, **kwargs) -> httpx.Response:
    for attempt in range(5):
        resp = client.request(method, url, **kwargs)
        if resp.status_code in (429, 503) or (resp.status_code == 409 and "Retry-After" in resp.headers):
            time.sleep(float(resp.headers.get("Retry-After", "1")))
            continue
        if resp.status_code >= 500:
            time.sleep(min(8, 0.5 * 2 ** attempt) * (1 - 0.25 * random.random()))
            continue
        return resp
    return resp
```

The SDKs do this for you, honouring `Retry-After` up to 60 seconds. `THROTTLED` (`Retry-After: 3600`) is not something to wait out in a loop; see [Errors](https://www.framequery.com/docs/errors#THROTTLED).

## Staying under the limits

- Poll with `GET /jobs/{jobId}?include=none` every 5 to 15 seconds, not faster, or use [webhooks](https://www.framequery.com/docs/webhooks).
- For many URLs, one `POST /jobs/batch` with up to 100 clips uses one job-creation request.
- To list work, page through `GET /jobs?state=...` rather than polling each job.
