Python asyncio semaphore: submit transcription jobs with a cap
Run Sume STT submits through asyncio.Semaphore and asyncio.to_thread, honor retry-after on 429, and keep one Idempotency-Key per clip. Tested code.

To submit many Sume transcription jobs from Python without hammering the API, wrap each POST in asyncio.to_thread, gate the calls with an asyncio.Semaphore, and sleep for retry-after on a 429. Send one Idempotency-Key per clip so a retry returns the same job. Run the entry point with asyncio.run(main(...)); top-level await does not work in a plain script. The retry and idempotency rules are in Errors and rate limits, read 2026-10-06.
What the semaphore limits
It limits requests in flight from your process, not Sume jobs. A job runs on Sume's side for seconds after the submit returns. Your workspace's accepted-job capacity is separate: when it is full you get 429 queue_full, which the loop below treats like any other 429 and retries after a pause. Keep the semaphore small (4 to 8) and let the server's answers set the pace.
| Limit | Where it lives | Signal |
|---|---|---|
| Requests in flight | Your semaphore | None; you choose it |
| Request rate | Sume API | 429 rate_limited with retry-after |
| Accepted jobs | Sume workspace plan | 429 queue_full |
The submitter
Tested against a local stub that returned one 429 with retry-after: 1; all six submits came back with a job id. Set SUME_API_KEY before running it for real.
import asyncio, os, requests
API = os.environ.get("SUME_API", "https://api.sume.com")
H = {"Authorization": f"Bearer {os.environ.get('SUME_API_KEY', 'test')}"}
async def submit(sem, clip, tries=5):
url, seconds, key = clip
body = {"audio_url": url, "duration_seconds": seconds}
async with sem:
for _ in range(tries):
r = await asyncio.to_thread(
requests.post, f"{API}/v1/stt-1.0/transcribe",
headers={**H, "Idempotency-Key": key}, json=body, timeout=30,
)
if r.status_code == 429:
await asyncio.sleep(float(r.headers.get("retry-after", "5")))
continue
r.raise_for_status()
return key, r.json().get("request_id")
return key, None
async def main(clips, limit=4):
sem = asyncio.Semaphore(limit)
return await asyncio.gather(*(submit(sem, c) for c in clips))
if __name__ == "__main__":
clips = [(f"https://media.sume.com/c{i}.wav", 5, f"stt-demo-{i}") for i in range(6)]
print(asyncio.run(main(clips)))After the submits
Save the (key, job id) pairs to disk before you do anything else. Then collect results with a status poll that obeys next_poll_after_seconds.
Sources
Related posts
More in Developers
- Python exceptions for Sume errors: retry on the class, not the status
Map the Sume error envelope code to two exception classes, Retryable and Fatal, so one except clause drives retries. Covers 409, 429, 402 and 503.
- Python pre-flight for a Short: catch Timeline schema errors offline
A Python check for a Sume Timeline body: even width and height, allowed fps, 1 to 1800 seconds, fade limits and start order. Run it before the plan call.
- Python: run one prompt on four AI video models and save the clips
A Python script that sends one prompt to Wan 3.0, Seedance 2.5, Kling 3 and MiniMax H3 on Sume, polls all four jobs and saves each MP4, with costs.
- Python TTS cost calculator: Sume job rounding vs per-character rates
A runnable Python function that prices narration lines on Sume (cent rounding, 1-cent minimum) and at flat per-million rates for MAI-Voice-2.1 and Flash.
Written by Sume