urllib3 Retry on POST: retry a Sume submit only with a key

A urllib3 Retry config that retries 429, 502, 503 and 504 on a Sume submit, and a wrapper that refuses to send a POST without an Idempotency-Key.

4 min readSume
All posts

urllib3 does not retry POST by default, and that default protects you from double charges. To retry a Sume submit, add POST to allowed_methods on the Retry object and make your own wrapper refuse any POST that has no Idempotency-Key. With a key, a retry that reaches Sume returns the original job with idempotency_hit true instead of creating a second paid job.

Which statuses to retry

Retry only the statuses that mean try again. Do not put 402 or 400 in the list: those need a human or a code change.

Retry decisions for a Sume submit, from the docs read 2026-10-08
StatusMeaningRetry with the same key?
402insufficient_creditsNo, add credits first
409idempotency_conflict (same key, different body)No, fix the key or the body
409idempotency_key_in_useYes, after about 1 second
429rate_limited or queue_fullYes, wait for retry-after
503provider_capacity_exceededYes, the failed create released the key

The client

The sample submits a 3-second Gemini Omni Flash 1.1 draft at 360p through the Video Router. At the billed 360p rate of $0.0375 per second (provider list $0.03 x 1.25), 3 seconds cost $0.1125. Set raise_on_status to False so you can read the final error body instead of catching MaxRetryError. The Retry-After header is honored by default.

import json, os, uuid
import urllib3
from urllib3.util import Retry

retry = Retry(total=3, backoff_factor=1.0, raise_on_status=False,
              status_forcelist=[429, 502, 503, 504],
              allowed_methods=frozenset({"GET", "POST"}))
http = urllib3.PoolManager(retries=retry, timeout=urllib3.Timeout(connect=5, read=60))

def submit(path, body, key):
    if not key:
        raise ValueError("refusing a POST without an Idempotency-Key")
    r = http.request("POST", "https://api.sume.com/v1" + path,
        body=json.dumps(body).encode(),
        headers={"x-api-key": os.environ["SUME_API_KEY"],
                 "Content-Type": "application/json", "Idempotency-Key": key})
    return r.status, json.loads(r.data)

status, out = submit("/video-router/generate", {
    "model": "gemini-omni-flash-1.1", "prompt": "A slow pan over a ceramic mug",
    "resolution": "360p", "duration": 3, "mode": "async"}, str(uuid.uuid4()))
print(status, out.get("data", out).get("request_id"))

Keep one key per intent

Generate the key once per piece of work, before the first attempt, and store it with your own record. The wrapper above receives it as an argument for that reason: if a fresh uuid were made inside the retry loop, every retry would be a new paid job. When the process restarts, read the stored key and resubmit with it, or look the job up before you resubmit. The same rule covers a worker that crashes after the request leaves but before the response arrives: the key is what lets the second attempt find the first job instead of paying for another.

  • Retry-After is read by urllib3 on 429 and 503 and overrides the backoff for that attempt.
  • A 4xx other than 409 idempotency_key_in_use and 429 is not retryable.
  • If all retries fail, the last response is still in your hands: log error.code and error.request_id.
  • A client timeout does not cancel the job, so a read timeout can still leave a running job behind the key.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume