Skip to main content

Rate Limits & Quotas

ViralSync enforces three independent resource limits. Understanding the difference between them helps you build more resilient integrations.


Three types of limits

LimitWhat it capsApplies to
API rate limitHTTP requests per minuteAll endpoints, all plans
Concurrent job limitSimultaneously running render jobsEnforced per account
Render minute quotaOutput video duration per monthEnforced per billing cycle

API Rate Limit

120 requests per 60-second sliding window per API key.

All endpoints share this limit — GET /v1/jobs, POST /v1/render/jobs, GET /v1/usage, etc. The window slides continuously; there is no fixed reset time.

Rate limit response

When you exceed the limit, you receive:

HTTP/1.1 429 Too Many Requests
Retry-After: 14
Content-Type: application/json

{
"error": "Rate limit exceeded. Retry after 14 seconds.",
"code": "RATE_LIMITED"
}

The Retry-After header tells you how many seconds to wait before your next request will succeed.

SSE connections and rate limits

GET /v1/jobs/{jobId}/progress (the SSE streaming endpoint) is a long-lived connection, not a repeated request. Establishing the SSE connection counts as one request; the stream itself does not consume further rate limit budget.


Concurrent Job Limit

The number of render jobs that can be in running state simultaneously on your account.

PlanConcurrent Jobs
Free1
Pro3
Business6
EnterpriseUnlimited

When you attempt to submit a new render job while at your limit:

{
"error": "Concurrent job limit reached. Wait for a running job to complete.",
"code": "CONCURRENCY_LIMIT_EXCEEDED"
}

How to handle this: Poll GET /v1/jobs?status=running&limit=10 to monitor when a slot opens, or subscribe to webhook events that fire when jobs complete. Then retry your submission.


Render Minute Quota

Monthly quota of output video duration. Measured in minutes. Resets on the 1st of each month UTC.

PlanRender Min / MonthDaily Cap
Free305 min / day
Pro200None
Business600None
EnterpriseUnlimitedNone

See Render Minutes & Billing → for full details.


Simple retry with backoff (JavaScript)

async function withRetry(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fn();

if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '5', 10);
if (attempt < maxAttempts) {
console.log(`Rate limited. Retrying in ${retryAfter}s...`);
await sleep(retryAfter * 1000);
continue;
}
}

if (res.status >= 500 && attempt < maxAttempts) {
const backoff = Math.min(2 ** attempt * 1000, 30000);
await sleep(backoff);
continue;
}

return res;
}
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Usage
const response = await withRetry(() =>
fetch('https://api.viralsync.io/v1/render/jobs', {
method: 'POST',
headers: { 'x-api-key': process.env.VIRALSYNC_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
);

Simple retry with backoff (Python)

import time
import requests

def with_retry(fn, max_attempts=3):
for attempt in range(1, max_attempts + 1):
response = fn()

if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
if attempt < max_attempts:
print(f'Rate limited. Retrying in {retry_after}s...')
time.sleep(retry_after)
continue

if response.status_code >= 500 and attempt < max_attempts:
backoff = min(2 ** attempt, 30)
time.sleep(backoff)
continue

return response

return response

# Usage
response = with_retry(lambda: requests.post(
'https://api.viralsync.io/v1/render/jobs',
headers={'x-api-key': os.environ['VIRALSYNC_API_KEY']},
json=payload
))

Distinguishing limits in application code

const res = await submitRenderJob(payload);

if (!res.ok) {
const { code } = await res.json();

switch (code) {
case 'RATE_LIMITED':
// Transient — back off and retry
const retryAfter = res.headers.get('Retry-After');
await sleep(parseInt(retryAfter, 10) * 1000);
break;

case 'CONCURRENCY_LIMIT_EXCEEDED':
// Transient — wait for a job slot to open
await waitForJobSlot();
break;

case 'RENDER_QUOTA_EXCEEDED':
// Persistent until quota resets — notify user
notifyUser('Monthly render quota reached. Upgrade or wait until the 1st.');
break;

case 'RENDER_QUOTA_DAILY_EXCEEDED':
// Resets at midnight UTC — notify user
notifyUser('Daily render cap reached. Resets at midnight UTC.');
break;

case 'CONCURRENCY_LIMIT_EXCEEDED':
// Upgrade required or wait
break;
}
}

Best practices

Poll efficiently. When waiting for a job to complete, poll GET /v1/jobs/{jobId} every 3–5 seconds — not more frequently. For real-time updates, use the SSE endpoint instead (GET /v1/jobs/{jobId}/progress).

Use webhooks in production. Webhook delivery does not count against your API rate limit. Configure a callbackUrl on your render job and let ViralSync push completion events to you.

Cache job status. If multiple parts of your application query the same job, cache the status client-side and invalidate on webhook receipt rather than making redundant API calls.

Spread bulk submissions. If you need to submit many render jobs at once, space them out by 1–2 seconds to avoid hitting the rate limit. Given the 120 req/min ceiling, you can sustain 2 submissions per second safely.