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.

5 min readSume
All posts

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.

Which limit does what, from Generation admission (docs.sume.com), read 2026-10-06.
LimitWhere it livesSignal
Requests in flightYour semaphoreNone; you choose it
Request rateSume API429 rate_limited with retry-after
Accepted jobsSume workspace plan429 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

All Developers posts

Written by Sume