Skip to content

Rate limits

Every API key is limited to 60 requests per minute, counted over a trailing window.

The limit counts requests, not rigs, so polling counts too. This is the main thing to design around: a naive batch that polls every job every second will hit the limit long before it runs out of credits.

Headers

Every authenticated response reports where you stand:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 2026-08-30T19:45:02.113Z

Read X-RateLimit-Remaining and slow down as it approaches zero, rather than waiting to be refused.

When you exceed it

json
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit of 60 requests per minute exceeded.",
    "retry_after_seconds": 60
  }
}

HTTP 429. Wait for the window to roll over and continue; nothing is lost, and no credits were spent on the refused request.

Rate limit vs capacity

These are different signals and are deliberately kept distinct:

429 rate_limited means you sent too many requests. Slow down.

503 backend_unavailable means the rigger is busy. Your pacing is fine; retry with backoff.

Some APIs report capacity pressure as a rate limit, which leads you to throttle a client that was never the problem. We don't.

Running batches

The pattern that stays comfortably inside the limit: submit with modest concurrency, then poll on a schedule rather than in a tight loop.

python
import time, requests
from concurrent.futures import ThreadPoolExecutor

API = "https://api.cinevva.com/v1"

def rig_batch(model_urls, api_key, concurrency=4, poll_interval=10):
    headers = {"Authorization": f"Bearer {api_key}"}

    def submit(url):
        r = requests.post(f"{API}/rigs", headers=headers,
                          json={"model_url": url}, timeout=30)
        r.raise_for_status()
        return r.json()["id"]

    with ThreadPoolExecutor(max_workers=concurrency) as pool:
        pending = set(pool.map(submit, model_urls))

    done = {}
    while pending:
        # One poll per job per interval. With a 10s interval this supports
        # ~10 concurrent jobs inside the 60/min budget, with headroom.
        time.sleep(poll_interval)
        for rig_id in list(pending):
            job = requests.get(f"{API}/rigs/{rig_id}", headers=headers, timeout=30).json()
            if job["status"] in ("succeeded", "failed"):
                done[rig_id] = job
                pending.discard(rig_id)
    return done

Two habits keep this well-behaved. Poll each job on an interval matched to the engine (5s for Fast, 15–20s for Pro) rather than as fast as the loop allows. And drop finished jobs out of the poll set immediately, which the example does by discarding from pending.

Need more?

The limit exists to stop runaway loops, not to ration legitimate work. If you have a pipeline that genuinely needs more sustained throughput, get in touch with a rough shape of the workload and we will raise it.