Short answer: do not create an unlimited number of threads. Give each business operation a unique
Idempotency-Key, submit withaccept_mode=async, persist the returnedrequest_id, and process work through a bounded worker pool. A single key is commonly limited to five concurrent generation tasks.
Four objects in a batch system
| Object | Example | Purpose |
|---|---|---|
| Business batch | batch_20261027_a | A group delivered together |
| Business item | brief_001 | One prompt or catalogue entry |
| Generation request | request_id | Asynchronous acceptance receipt |
| Song | song_id | Queryable and downloadable result |
Queue first, then use bounded concurrency
- Start with three or four generation workers; approach the documented limit only after observing real behaviour.
- Keep polling workers separate from generation workers.
- Query several song IDs in one request using the comma-separated
song_idsvalue. - Estimate delivery time from concurrency multiplied by average completion time, not from a tight retry loop.
Python worker-pool example
import concurrent.futures
import requests
BASE = "https://www.suno-api.io"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def submit_one(item):
response = requests.post(
f"{BASE}/api/music/create",
headers={
**HEADERS,
"Content-Type": "application/json",
"Idempotency-Key": item["idempotency_key"],
},
json={
"description": item["description"],
"model": "suno-v6",
"accept_mode": "async",
},
timeout=60,
)
response.raise_for_status()
data = response.json()["data"]
return item["brief_id"], data["request_id"], data["status"]
items = [
{
"brief_id": f"brief_{i:03d}",
"idempotency_key": f"batch-20261027-{i:03d}",
"description": f"short-film score {i}: suspense strings, no vocals",
}
for i in range(1, 21)
]
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
accepted = list(pool.map(submit_one, items))
print("accepted", len(accepted))The submit worker stores the receipt and finishes. A separate polling worker later checks GET /api/music/requests/{request_id}. This separation makes a restart safe and prevents create calls from being repeated inside a polling loop.
Poll the acceptance receipt
def refresh(request_id):
response = requests.get(
f"{BASE}/api/music/requests/{request_id}",
headers=HEADERS,
timeout=60,
)
response.raise_for_status()
data = response.json()["data"]
for task in data.get("tasks", []):
if task.get("song_id"):
save_song(request_id, task["index"], task["song_id"], task["status"])
return data["status"]On partial, save completed task items rather than waiting for both. On failed, record the error and stop that request. Only a new business decision should create a new paid task.
Retry matrix
| Case | Retry? | Action |
|---|---|---|
| 400 validation error | No | Fix the input |
| 401 / 403 | No | Fix key or account state |
| 402 insufficient balance | No | Top up and resume the queue |
| 429 rate limited | Yes | Honor retry_after, reduce concurrency, back off |
| 5xx temporary error | Yes | Exponential backoff with the same idempotency key |
| Create timeout | Check first | Look for an existing request or song before resubmitting |
Capacity estimate
If a batch has 100 business items and each creates one generation that returns two songs, expect 100 paid generation requests and up to 200 song results. Delivery time is driven primarily by the number of concurrent generation jobs and upstream completion time, not by how often the poller runs.
FAQ
Does a two-song response count as two concurrent generations?
No. Concurrency is based on generation requests. One request is one generation job.
Can I retry immediately after a 429?
No. Wait for the retry window, lower active concurrency, and preserve the idempotency key.
How do I recover after a process restart?
Load request_id and song_id values from durable storage and continue querying. Never depend on an in-memory queue.
Related: cURL and Postman, response fields, authentication and limits, and the acceptance receipt mode.
¥0.6 per generation, 2 tracks, unlimited downloads
Runs on the Suno V6 engine. Download MP3 or WAV as often as you like. Signing up needs an email address, not a phone number.


