AI Video API Rate Limits and Quotas: What Developers Need to Know
You're running a batch of 50 video generation jobs for a product demo — the first 20 complete fine, then your application starts returning HTTP 429 errors and the remaining jobs hang indefinitely. Understanding rate limits before you hit them at production scale is the difference between a smooth launch and an on-call incident at 2 AM.
This article covers how rate limits work in AI video generation APIs, what the common limit dimensions are, and how to write retry and queue logic that handles them without losing jobs or burning budget.
How Do Rate Limits Work in AI Video Generation APIs?
Rate limits on video generation APIs typically operate on two or three dimensions simultaneously: requests per minute (RPM), concurrent jobs, and sometimes output seconds per rolling window. Unlike LLM APIs where a single request completes in milliseconds, video jobs take 20–120 seconds — which means concurrent job limits are the constraint you'll hit first, not RPM.
The specific limits depend on your plan tier. Free and entry-level plans typically cap at 1–3 concurrent jobs; production plans allow more. Check your plan's quota page for the exact values — providers adjust these as infrastructure capacity grows.
What Limit Dimensions Should You Monitor?
Concurrent job limit is the most common bottleneck for video APIs. If your plan allows 5 concurrent jobs and you submit 20, the API accepts the first 5 and returns 429 for the rest. Accepted jobs run to completion even if you hit the limit on submission — the rejection is on new submissions, not running jobs.
Requests per minute (RPM) governs how fast you can submit new jobs, independently of how many are running. A 60 RPM limit means you can submit one new job per second on average.
Output seconds per hour (or day) — some video APIs bill by the second and also cap throughput at N seconds of video output per rolling time window. At $0.060/s for Veo 3.1 Lite 720p, a 1,000-second/hour cap translates to a $60/hour ceiling on that model before limits engage.
Monthly quota — soft ceilings that some providers apply as a guardrail. If you're approaching monthly limits, contact support proactively rather than hitting the wall unexpectedly mid-job.
How Do You Handle 429 Errors? Retry With Exponential Backoff
When you receive a 429, the correct response is to wait and retry — not to loop aggressively or silently drop the job. A standard exponential backoff implementation:
import requests, time, random
def submit_video_job(payload, headers, max_retries=5):
url = "https://api.genrelay.ai/v1/videos/generations"
delay = 2 # initial delay in seconds
for attempt in range(max_retries):
resp = requests.post(url, headers=headers, json=payload)
if resp.status_code == 200:
return resp.json()["id"]
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", delay))
jitter = random.uniform(0, 1)
wait = retry_after + jitter
print(f"Rate limited. Waiting {wait:.1f}s (attempt {attempt+1}/{max_retries})")
time.sleep(wait)
delay *= 2 # exponential backoff
continue
# Non-retryable error — raise immediately
resp.raise_for_status()
raise Exception(f"Failed after {max_retries} retries")
Always read the Retry-After response header first — the API tells you the minimum wait time. Add random jitter (0–1 second) to prevent a thundering herd when multiple clients retry simultaneously after a shared limit event.
How Do You Build a Concurrent Job Queue That Stays Within Limits?
For batch workloads — generating videos for 200 product listings or processing a queue of user-submitted prompts — you need a client-side queue that respects the concurrent job limit without blocking your main thread or polling aggressively.
Here's a Python implementation using asyncio and a semaphore to cap concurrency:
import asyncio, aiohttp, random
GENRELAY_KEY = "YOUR_GENRELAY_KEY"
CONCURRENT_LIMIT = 5 # match your plan's concurrent job cap
POLL_INTERVAL = 8 # seconds between status checks
async def generate_video(session, semaphore, prompt, index):
async with semaphore:
headers = {"Authorization": f"Bearer {GENRELAY_KEY}"}
# Submit with retry on 429
for attempt in range(5):
async with session.post(
"https://api.genrelay.ai/v1/videos/generations",
headers=headers,
json={"model": "grok-imagine-1.0", "prompt": prompt, "duration": 5}
) as resp:
if resp.status == 429:
wait = int(resp.headers.get("Retry-After", 10)) + random.uniform(0, 1)
await asyncio.sleep(wait)
continue
data = await resp.json()
job_id = data["id"]
break
else:
raise Exception(f"[{index}] Could not submit after retries")
# Poll until complete
while True:
await asyncio.sleep(POLL_INTERVAL)
async with session.get(
f"https://api.genrelay.ai/v1/videos/generations/{job_id}",
headers=headers
) as poll:
result = await poll.json()
if result["status"] == "completed":
print(f"[{index}] Done: {result['output']['url']}")
return result["output"]["url"]
elif result["status"] == "failed":
raise Exception(f"Job {job_id} failed: {result.get('error')}")
async def run_batch(prompts):
semaphore = asyncio.Semaphore(CONCURRENT_LIMIT)
async with aiohttp.ClientSession() as session:
tasks = [generate_video(session, semaphore, p, i) for i, p in enumerate(prompts)]
return await asyncio.gather(*tasks, return_exceptions=True)
# Usage — 200 prompts, max 5 concurrent at any time
prompts = ["A sunset over mountains", "City traffic time-lapse"] # extend as needed
results = asyncio.run(run_batch(prompts))
# Identify failures without crashing the batch
failures = [(i, r) for i, r in enumerate(results) if isinstance(r, Exception)]
print(f"{len(failures)} failed jobs: {failures}")
The semaphore ensures no more than CONCURRENT_LIMIT jobs run simultaneously regardless of batch size. gather(..., return_exceptions=True) means a failed job returns an exception object rather than crashing the whole batch. Filter results to identify and re-queue failures.
For a deeper dive on polling patterns across models, see Async video generation API — how to poll for results.
How Do Limits Vary by Model?
Different models have different typical processing times, which affects how quickly your concurrent slots cycle. A model that finishes in 20 seconds turns over 5 slots 3× per minute; one that takes 120 seconds turns over 5 slots once every 2 minutes.
| Model | Typical processing (5s clip) | Billing rate (720p) | Slot turnover rate |
|---|---|---|---|
| Grok Imagine 1.0 | ~20–40s | $0.010/s | Fastest |
| Omni Flash | ~30–60s | $0.10/gen | Moderate |
| Grok Imagine 1.5 (i2v) | ~30–60s | $0.022/s | Moderate |
| Veo 3.1 Lite | ~60–120s | $0.060/s | Slowest |
For high-volume bulk generation where you're constrained by concurrent job limits, Grok Imagine 1.0 at $0.010/s offers the fastest slot turnover and the lowest cost per second. Veo 3.1 Lite produces higher visual quality but occupies slots for longer — better for lower-volume, quality-sensitive use cases. For the full integration setup, see How to integrate AI video generation into your app and the Veo model page.
Rate Limit vs Quota Exhaustion — Don't Confuse Them
Both return HTTP 429, but they require different handling:
- Rate limit: temporary. Wait
Retry-Afterseconds and retry. - Quota exhaustion: non-temporary within the current billing window. Retrying does nothing — you need to upgrade your plan or wait for the quota to reset.
Check the error body to distinguish them:
if resp.status_code == 429:
body = resp.json()
error_code = body.get("error", {}).get("code", "")
if error_code == "quota_exceeded":
# Non-retryable — escalate to application level
raise QuotaExhaustedError(
"Monthly video quota reached. Upgrade plan or wait for billing reset."
)
else:
# Temporary rate limit — retry with backoff
wait = int(resp.headers.get("Retry-After", 10))
time.sleep(wait)
Conflating these is a common mistake: retrying a quota exhaustion loops indefinitely and wastes your retry budget without ever succeeding.
How to Monitor Rate Limit Headroom
Add visibility into your limit consumption so you can throttle proactively rather than react to failures:
- Log
X-RateLimit-RemainingandX-RateLimit-Resetheaders on each response if the API provides them. These show how many requests remain in the current window. - Track concurrent job count in application state — a counter incremented on submission, decremented on completion or failure.
- Proactive throttling: if remaining drops below 20% of your limit, introduce a delay between new submissions rather than waiting for a 429.
- Dashboard visibility: in the GenRelay console, current quota usage is visible under Usage → API Quotas, showing concurrent job usage and monthly compute consumed.
For applications with predictable throughput requirements (e.g., processing a nightly batch of 500 videos), run a volume estimate before production:
5 concurrent slots × (60s / 40s avg processing) = ~7.5 jobs/minute throughput
500 jobs ÷ 7.5 = ~67 minutes to complete the batch
Cost: 500 × 5s × $0.010/s = $25 (Grok 1.0 at 720p)
This kind of pre-calculation prevents surprises at runtime.
FAQ
What's the difference between a rate limit and a quota?
Rate limits are short-window throughput ceilings (requests per minute, concurrent jobs). Quotas are longer-window usage ceilings (seconds of video per day, monthly compute budget). Both return HTTP 429, but quotas can't be resolved by waiting a few seconds — they require a plan change or waiting for the billing period to reset.
Do rate limits reset at the top of each minute?
For RPM limits, yes — they typically use a rolling 60-second window or a fixed-minute reset. Concurrent job limits don't reset on a clock; a slot frees when a job completes or fails.
Will rejected jobs (429 on submission) affect my billing?
No. You're billed only for jobs that successfully start processing. A 429 rejection on submission has no billing impact.
Can I request higher concurrent job limits?
Yes. For production workloads that consistently hit concurrent caps, contact GenRelay support to discuss higher limits or reserved capacity. Include your expected job volume and average duration in the request.
Does polling count against my rate limits?
Status check requests (GET on a job ID) typically have a separate, higher limit than job submission requests. Polling every 5–10 seconds for active jobs is well within normal bounds — aggressive polling (sub-second intervals) may trigger a separate polling rate limit.