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
textHTTP/1.1 429 Too Many RequestsRetry-After: 5
Retry-After is in seconds, rounded up. The body's retry_after_ms is more precise.
Backoff strategy
tsasync 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 — 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).