Hangfire AutomaticRetry for Sume: stable key, no double bill
Hangfire retries failed jobs 10 times by default. Cap attempts on a Sume submit job and send one stable Idempotency-Key so a retry never makes a second job.

Hangfire retries a failed background job automatically, so a Sume submit method needs two things: a stable Idempotency-Key built from your own record and an explicit [AutomaticRetry] limit. Hangfire's docs say the filter applies globally with 10 retry attempts by default and that [AutomaticRetry(Attempts = 0)] turns retries off for a method (read 2026-10-04). Ten tries at a 402 is ten pointless requests, and ten tries at a timeout without an idempotency key is a path to duplicate jobs.
With the same key on each attempt, Sume replays the original submit instead of creating a second job, per Jobs and results.
What does the job class look like?
Throw for retryable statuses so Hangfire retries, and return normally for permanent ones so it does not.
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using Hangfire;
public class SumeSubmit(HttpClient http)
{
[AutomaticRetry(Attempts = 4, DelaysInSeconds = new[] { 10, 30, 120, 300 })]
public async Task SubmitHero(string sku)
{
var key = Environment.GetEnvironmentVariable("SUME_API_KEY")
?? throw new InvalidOperationException("SUME_API_KEY missing");
using var req = new HttpRequestMessage(HttpMethod.Post,
"https://api.sume.com/v1/image-1.0/generate");
req.Headers.Add("x-api-key", key);
req.Headers.Add("Idempotency-Key", $"hero-{sku}-v1");
req.Content = JsonContent.Create(new { prompt = $"Product hero shot, {sku}", mode = "async" });
using var res = await http.SendAsync(req);
if (res.IsSuccessStatusCode) return;
var transient = res.StatusCode == (HttpStatusCode)429 || (int)res.StatusCode >= 500;
if (transient) throw new HttpRequestException($"sume {(int)res.StatusCode}");
// 400, 402, 403: retrying cannot help; record it and stop
Console.Error.WriteLine($"sume {(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
}
}Which retries are safe?
| Outcome | Throw? | Reason |
|---|---|---|
| 2xx | No | Job created; store request_id |
| 429 or 5xx | Yes | Transient; the same key replays safely |
| 409 idempotency_key_in_use | Yes | The earlier request is still in flight |
| 402 or 403 | No | Funding or scope; a person must act |
| 400 | No | The same body will fail again |
| Timeout or network error | Yes | The earlier request may have been accepted; the key protects you |
Where do results come from?
Keep this job to the submit. Take the result from a signed webhook or from a separate polling job that reads /v1/jobs/{id}/status using next_poll_after_seconds. Mixing a long wait into a retried job means a retry can start while the first attempt is still waiting.
What should you configure around it?
- Register
HttpClientthroughIHttpClientFactorywith a 30 second timeout. - Use a different attribute value, or
Attempts = 0, on any method that calls Sume without a key. - Log the
x-sume-request-idheader on failures to quote to support.
Sources
Related posts
More in Developers
- HappyHorse video-edit ids and the Sume edit route
Alibaba Model Studio lists HappyHorse 1.1 t2v, i2v and r2v ids and points edits elsewhere. Sume has no HappyHorse ids; its edit route is Gemini Omni Flash 1.1.
- HeyGen callback_url vs a signed Sume webhook for a finished video
HeyGen lets you pass callback_url to skip polling. Sume adds mode webhook with a public HTTPS webhook_url and signs each delivery, so verify it.
- A holiday ad brief as one JSON file that drives every Sume call
Keep one JSON brief with the product, offer, date and ratios, and have a script read it for each Sume call so every asset says the same thing.
- How big is a 3-minute Short at YouTube's 8 Mbps? About 189 MB
A 180-second 1080p Short at YouTube's 8 Mbps video and 384 kbps audio is about 189 MB. Here is the arithmetic, the TikTok ad caps, and the Sume probe field.
Written by Sume