Python urllib: retry a Sume video submit on 429 and 503, no requests

A standard-library Python function that retries a Sume video submit on 429 and 503, reusing one Idempotency-Key and reading Retry-After. Tested on a stub.

5 min readSume
All posts

To retry a Sume video submit with only the Python standard library, loop over urllib.request.urlopen, catch HTTPError, retry on 429 and 503 only, wait for the Retry-After header when one is present, and send the same Idempotency-Key on every attempt. The function below does that in 25 lines with no third-party package. A replay with the same key returns the original job, so a retry cannot bill a second clip.

Everything else is a final answer. A 400, 402, 404, or 409 will not change on a second attempt, so the function raises on them at once with the status and the Sume error code.

Which statuses are worth a retry

A queue_full 429 deserves more than a short sleep. It means that every accepted slot in the workspace is in use, so the delay is the time one running job takes, not a few seconds. The function below treats it like any other 429 and gives up after four attempts; a real batch runner should check capacity before it submits.

Submit errors and the right reaction (Sume docs read 2026-10-08)
StatusCodeReaction
429rate_limitedRetry with backoff and the same key
429queue_fullWait for jobs to finish or cancel queued ones, then retry with the same key
503provider_capacity_exceededRetry later with the same key, unless the error says not to
409idempotency_conflictStop. The key was used for a different payload
402insufficient creditsStop. Add funds first

The function

Set SUME_API_KEY and call it with a body and a key. The key is yours to choose, and it should name the job, not the attempt. The wait is Retry-After in seconds when that parses as a number, and 1, 2, 4 seconds otherwise. An HTTP-date value falls into the second case here; the linked Retry-After post shows how to parse it.

import json, os, time, urllib.error, urllib.request

BASE = os.environ.get("SUME_BASE", "https://api.sume.com")

def submit(body, key, attempts=4):
    for n in range(attempts):
        req = urllib.request.Request(
            BASE + "/v1/videos", method="POST", data=json.dumps(body).encode(),
            headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
                     "Content-Type": "application/json", "Idempotency-Key": key})
        try:
            with urllib.request.urlopen(req, timeout=35) as r:
                return json.load(r)
        except urllib.error.HTTPError as e:
            code = json.load(e).get("error", {}).get("code")
            if e.code not in (429, 503) or n == attempts - 1:
                raise RuntimeError(f"{e.code} {code}") from None
            try:
                wait = float(e.headers.get("Retry-After"))
            except (TypeError, ValueError):
                wait = 2 ** n
            print(f"{e.code} {code}: waiting {wait}s")
            time.sleep(wait)

print(submit({"model": "seedance-2.5", "prompt": "A mug on a desk"}, "mug-v1"))

What the stub run showed

The code was run against a local stub that answers 429 with Retry-After: 1 on the first call, 503 on the second, and 202 on the third for one key. The output was two waiting lines, one of 1.0 seconds and one of 2 seconds, then the job envelope. No Sume account was involved.

The timeout is 35 seconds. A synchronous submit can hold the connection for at most 30 seconds, so a client timeout below that would cut off a valid wait.

Limits of the sample

  • Transport errors such as a reset connection are not caught. Add urllib.error.URLError to the except clause; the same key keeps that retry safe.
  • Four attempts is a choice. Raise it for a batch runner, and stop earlier for a request a person is waiting on.
  • The function returns after the submit. Polling is a separate step, covered in the posts on poll intervals.

Choosing the key

The key decides what counts as the same job. Build it from the business fact, such as an order number plus a version, and not from a random value made inside the retry loop. A random key per attempt would defeat the purpose, because each attempt would then look like a new job.

If you do change the request, change the key with it. Sume answers 409 idempotency_conflict when a key comes back with a different body, and that error is a signal to stop and look, not to retry. Keep the keys in your own table next to the job id that came back, so a crashed process can find its work again.

Finally, remember the order of events in a real batch: check capacity, submit, store the job id, then poll or wait for a webhook. Retrying the submit is only the second step.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume