Dramatiq retry_when for Sume: retry only retryable errors
A Dramatiq actor with retry_when that retries 429 and 5xx but not 402 or 400, and sends one stable Idempotency-Key so a retried Sume submit is not billed twice.

In Dramatiq, give the Sume submit actor a retry_when predicate that returns true only for errors Sume marks retryable, and derive one Idempotency-Key from your own record so every attempt sends the same key. Dramatiq's reference describes retry_when as a callable taking the retry count and the exception, returning a bool, and says it takes precedence over max_retries when set (read 2026-10-04).
The retry decision comes from Sume's error docs: 429 and 503 are transient, while a 402 insufficient_credits or a 400 validation error will fail the same way every time.
What does the actor look like?
The exception carries the HTTP status and the retryable flag from the body, so the predicate stays a single line.
import os, dramatiq, requests
class SumeError(Exception):
def __init__(self, status, retryable, code):
super().__init__(f"{status} {code}")
self.status, self.retryable, self.code = status, retryable, code
def should_retry(retries_so_far: int, exc: BaseException) -> bool:
if isinstance(exc, SumeError):
return exc.retryable or exc.status in (429, 503)
return isinstance(exc, (requests.ConnectionError, requests.Timeout))
@dramatiq.actor(retry_when=should_retry, min_backoff=5_000, max_backoff=300_000)
def submit_hero(sku: str) -> None:
r = requests.post("https://api.sume.com/v1/image-1.0/generate", timeout=30,
headers={"x-api-key": os.environ["SUME_API_KEY"],
"Idempotency-Key": f"hero-{sku}-v1"},
json={"prompt": f"Product hero shot, {sku}", "mode": "async"})
if r.status_code >= 400:
e = r.json().get("error", {})
raise SumeError(r.status_code, bool(e.get("retryable")), e.get("code"))
print(r.json()["data"]["request_id"])Which errors deserve a retry?
| Status and code | Retry? | Why |
|---|---|---|
| 429 rate_limited | Yes, after retry-after | The write budget refills; the job was not created |
| 409 idempotency_key_in_use | Yes, after about a second | Another request with the same key is in flight |
| 503 studio_agent_upstream_unavailable | Yes | A Sume-side outage; same key is safe |
| 402 insufficient_credits | No | Needs funding by a person |
| 403 insufficient_scope | No | Needs a different key |
| 400 validation error | No | Fix the input |
Why does the key matter more than the backoff?
A retry after a timeout may follow a request Sume already accepted. With the same Idempotency-Key, Sume replays the original response instead of creating a second job, so you are not billed twice. A key built from uuid4() inside the actor would be new on every attempt and defeat that. Build it from the SKU, the order id or another stable value you own.
What else should the actor avoid?
- Do not poll for the result inside the same actor; submit here and poll or take a webhook elsewhere.
- Set
max_backofflow enough that a retry lands within your own deadline. - Log the
request_idfrom the error body on every failed attempt.
Sources
Related posts
More in Developers
- Dropped connection mid-render: what happens to the Sume job
A dropped connection never cancels a Sume job. Wait again with jobs_wait on the same ids, or read the job status; never resubmit the paid create.
- "Each input_references entry needs image_url.url": the fix
A bare URL string or a missing url in input_references returns 400 invalid_request on Sume. The exact entry shape, reference ceilings per row, a helper.
- Mcp-Name header rules: rate-limit paid render tools at the gateway
MCP 2026-07-28 requires Mcp-Method and Mcp-Name headers on Streamable HTTP POSTs. A gateway can rate-limit paid render tools by name without reading the body.
- GPT Image 2.5 on ElevenLabs: 14 ratios plus auto. Sume lists 17
ElevenLabs offers 14 fixed ratios plus auto for GPT Image 2.5. Sume's normalized list has 17 plus auto. Read what each model accepts before sending one.
Written by Sume