Saltar a la navegación

Concurrency & Rate Limiting

Understanding rate limiting and concurrency

Rate Limiting

Request-rate limits count requests over a time window. These limits are separate from generation concurrency.

API Endpoints

EndpointRequest-rate limit
POST /v2/tts60 requests/min
POST /v2/tts/jobs60 requests/min, separately from synchronous TTS
POST /v2/voices10 requests/hour
POST /v2/assets/upload120 requests/min

Generation creation also enforces the plan’s concurrency limit described below. Inspect the error body instead of treating every 429 as the same limit.

When rate limited, the API returns a 429 response with a Retry-After header:

{
"statusCode": 429,
"errorCode": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please try again later.",
"error": "Too Many Requests",
"limit": 60,
"remaining": 0,
"resetTime": "2026-11-05T12:01:00.000Z",
"retryAfter": 42,
"retryAfterSeconds": 42
}

Concurrency

Concurrency refers to the number of generations that can be submitted/processed concurrently. Requests to create new generations will fail with a 429 error if the concurrency limit is exceeded. Batch records (approved organizations on Scale and above) do not count toward this limit; each active batch holds one slot of a separate batch limit.

A concurrency 429 looks like this:

{
"statusCode": 429,
"errorCode": "concurrency_limit_reached",
"message": "You have 3 active generations, which is your current concurrency limit of 3. Wait for an in-flight generation to finish (poll GET /v2/generations?status=PROCESSING) or upgrade your plan for higher limits: https://sync.so/billing/subscription",
"activeGenerations": 3,
"concurrencyLimit": 3,
"retryAfterSeconds": 20,
"docsUrl": "https://sync.so/docs/api-reference/guides/rate-limits",
"upgradeUrl": "https://sync.so/billing/subscription",
"billingUrl": "https://sync.so/billing/subscription"
}

The response also carries Retry-After, X-Sync-Concurrency-Limit and X-Sync-Active-Generations headers with the same values, so you can back off without parsing the body. Get Account returns your current limits.concurrency and activeJobs.generations.

To check your generations currently in PENDING/PROCESSING state, you can use the List Generations endpoint.

Concurrency limits are defined in the subscription plan. Usage-billed and credit-billing plans use the following defaults:

Usage-billed planCredit planConcurrent generations
FreeFree1
HobbyistHobby3
CreatorStarter10
GrowthPro20
ScaleTeam50
EnterpriseEnterprise50 or a higher custom allowance

Your plan default is the minimum concurrency available to your account. Custom allowances can raise these limits, but never lower them. Read concurrencyLimit or the X-Sync-Concurrency-Limit header on a concurrency_limit_reached response for your effective limit.

Handling 429 Errors

When you exceed a rate limit or concurrency limit, the API returns a 429 Too Many Requests response. This applies to both per-minute rate limits and concurrent generation limits.

What to do when you hit a 429:

Tell them apart by errorCode:

  • Rate limit (rate_limit_exceeded): Wait and retry. Honor the Retry-After header (or retryAfterSeconds and resetTime in the body); endpoint windows differ.
  • Concurrency limit (concurrency_limit_reached): You already have the maximum number of generations in PENDING or PROCESSING state for your plan. Wait retryAfterSeconds (20) or for an existing generation to complete before submitting a new one, or upgrade your plan for higher limits.

Do not retry 429 responses immediately in a tight loop. This wastes requests and delays recovery. Use the retry strategy below instead.

Retry Strategies

Exponential backoff is the recommended approach for handling both rate limit and transient errors from the Sync Labs lip sync API. Each retry waits progressively longer, reducing pressure on the API and improving your success rate.

import time
from sync import Sync
from sync.common import Audio, Video
from sync.core.api_error import ApiError
sync = Sync()
def create_with_retry(video_url: str, audio_url: str, max_retries: int = 5):
for attempt in range(max_retries):
try:
return sync.generations.create(
input=[Video(url=video_url), Audio(url=audio_url)],
model="lipsync-2",
)
except ApiError as e:
if e.status_code == 429 and attempt < max_retries - 1:
wait = 2 ** attempt # 1s, 2s, 4s, 8s, 16s
print(f"Rate limited. Retrying in {wait}s...")
time.sleep(wait)
else:
raise
response = create_with_retry(
"https://assets.sync.so/docs/example-video.mp4",
"https://assets.sync.so/docs/example-audio.wav",
)
print(f"Job submitted: {response.id}")
import { SyncClient, SyncError } from "@sync.so/sdk";
const sync = new SyncClient();
async function createWithRetry(videoUrl: string, audioUrl: string, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await sync.generations.create({
input: [
{ type: "video", url: videoUrl },
{ type: "audio", url: audioUrl },
],
model: "lipsync-2",
});
} catch (err) {
if (err instanceof SyncError && err.statusCode === 429 && attempt < maxRetries - 1) {
const wait = 2 ** attempt * 1000; // 1s, 2s, 4s, 8s, 16s
console.log(`Rate limited. Retrying in ${wait / 1000}s...`);
await new Promise((r) => setTimeout(r, wait));
} else {
throw err;
}
}
}
}
const response = await createWithRetry(
"https://assets.sync.so/docs/example-video.mp4",
"https://assets.sync.so/docs/example-audio.wav",
);
console.log(`Job submitted: ${response.id}`);

Optimizing Concurrency

To get the most out of your plan’s concurrent generation slots:

  • Monitor active jobs. Use the List Generations endpoint to check how many jobs are currently in PENDING or PROCESSING state before submitting new ones.
  • Queue on your side. Maintain a local queue and only submit a new generation when a slot frees up. This avoids wasted 429 responses.
  • Use webhooks. Configure a webhook to get notified when a generation completes, so you can immediately submit the next job without polling.
  • Batch large runs. With organization approval on Scale and Enterprise plans, the Batch API queues 20 to 500 short generations per call outside your generation concurrency, with results over hours. For a few generations at once, send POST /v2/generate requests in parallel within your concurrency limit instead.

For API rate limiting best practices and general error handling, see the Error Handling guide.