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
- A Python Enum for Sume job statuses and the terminal set
Sume jobs move through queued, processing, completed, failed and canceled. A tested Python Enum and terminal set so a poll loop stops on all three endings.
- 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: which Sume video models take 20 seconds at 1080p?
A short Python script reads GET /v1/videos/models and filters by duration and resolution, so new launches never break a hard-coded model list.
- Python compare_digest TypeError on a Sume signature header
compare_digest raises TypeError on non-ASCII str. A tested Python verifier for the Sume webhook signature that compares bytes and rejects an empty secret.
Written by Sume