Rate Limits & Quotas
ViralSync enforces three independent resource limits. Understanding the difference between them helps you build more resilient integrations.
Three types of limits
| Limit | What it caps | Applies to |
|---|---|---|
| API rate limit | HTTP requests per minute | All endpoints, all plans |
| Concurrent job limit | Simultaneously running render jobs | Enforced per account |
| Render minute quota | Output video duration per month | Enforced 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.
| Plan | Concurrent Jobs |
|---|---|
| Free | 1 |
| Pro | 3 |
| Business | 6 |
| Enterprise | Unlimited |
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.
| Plan | Render Min / Month | Daily Cap |
|---|---|---|
| Free | 30 | 5 min / day |
| Pro | 200 | None |
| Business | 600 | None |
| Enterprise | Unlimited | None |
See Render Minutes & Billing → for full details.
Recommended Retry Strategy
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.