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

# Rate limits

Per-endpoint rate caps and how to back off correctly.

# Rate limits

| Endpoint group | Per-user/min |
|---|---|
| `POST /api/v1/prepare` | 60 |
| `POST /api/assets/:id/regenerate` | 30 |
| `POST /api/projects/:id/deep-crawl` | 10 (Firecrawl-bounded) |
| `POST /api/projects/:id/detect-brand` | 30 |
| `POST /api/projects/:id/webhooks/test` | 30 |
| Other authenticated `/api` endpoints | 300 |

## Quota vs rate limit

Two independent limits apply:

- **Quota** — monthly generation cap (5 / 100 / 500 by tier). Hits return `429 { error: 'monthly limit reached', upgrade_url }`.
- **Rate limit** — burst protection. Hits return `429 { error: 'rate limited', retry_after_ms }`.

## Headers

```
HTTP/1.1 429 Too Many Requests
Retry-After: 5
```

`Retry-After` is in seconds, rounded up. The body's `retry_after_ms` is more precise.

## Backoff strategy

```ts
async function callWithBackoff<T>(
  fn: () => Promise<Response>,
  maxAttempts = 5,
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fn();
    if (res.status !== 429) {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    }
    const retryMs = Number(res.headers.get('Retry-After')) * 1000;
    await new Promise((r) => setTimeout(r, retryMs));
  }
  throw new Error('rate limit exceeded after max retries');
}
```

## Public proxy URLs

The proxy URL itself (`/v1/...`) has no per-IP rate limit, since edge cache absorbs repeat hits. The first generation per `(asset_id)` does count against monthly quota; subsequent same-URL requests are free cache hits.

## Allowlisted origin spam

If a third party scrapes proxy URLs and you see surprising quota burn, switch to [signed URLs](/concepts/allowlist-vs-signed) — they expire and can't be re-used past `exp`.

## SSE connection limits

Up to 5 minutes per connection. Reconnect with `Last-Event-ID` for longer streams. No per-asset connection cap (fan-out is in-process on the worker).