Effect 4.0.4 retryTransient honors Retry-After: key your Sume submit
Effect 4.0.4 makes HttpClient.retryTransient honor Retry-After, so a Sume 429 waits as told. It also retries POST on 5xx, so send an Idempotency-Key.

Yes. As of effect 4.0.4, published on 2026-10-13, HttpClient.retryTransient honors a Retry-After header, and Sume sends retry-after on a 429. So one line of Effect retries a rate-limited call at the time Sume asks for. The catch is that the same policy also retries POST requests on 500, 502, 503 and 504, so a Sume submit must carry an Idempotency-Key, or a retry can start a second paid job.
The 4.0.4 release notes list two relevant lines: Retry-After is honored in HttpClient.retryTransient, and HttpClient.withRateLimiter now reads Retry-After HTTP dates correctly. Retry helpers in this module are marked unstable in the source, so pin the version. I ran the script below on 4.0.4 against a local stand-in for the API that answers the first submit with a 429 and retry-after.
What does retryTransient retry?
The source treats these statuses as transient: 408, 429, 500, 502, 503 and 504. It also retries timeouts and transport errors. A Retry-After header sets the minimum delay before the next attempt. Compare that with what Sume documents on its errors page: on 429 use retry-after when present, and do not retry unsafe submits without an Idempotency-Key.
| Status | retryTransient | Sume submit guidance |
|---|---|---|
| 429 | Retries, waits for Retry-After | Retry with the same Idempotency-Key |
| 500, 502, 503, 504 | Retries | Retry only with the same Idempotency-Key |
| 408 or a dropped connection | Retries | The job may exist: the key makes the replay safe |
| 402 | Not retried | Stop: out of credits |
How do you wire it up?
Build the client once: add the bearer token, then retryTransient, then filterStatusOk so a non-2xx reply becomes an error. Both are applied to the same client, so the retry sees the error the status filter raises; with the calls in the order shown in the script, the run retried the 429 as intended. The Idempotency-Key is set on the request before execute, so every attempt sends the same value. wan-3.0 accepts 2 to 30 seconds, so duration: 5 at 480p is valid.
import { Effect } from "effect";
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
const base = process.env.SUME_BASE ?? "https://api.sume.com";
const program = Effect.gen(function* () {
const http = (yield* HttpClient.HttpClient).pipe(
HttpClient.mapRequest(HttpClientRequest.bearerToken(process.env.SUME_API_KEY!)),
HttpClient.retryTransient({ times: 3 }),
HttpClient.filterStatusOk,
);
const submit = HttpClientRequest.post(`${base}/v1/videos`).pipe(
HttpClientRequest.setHeader("Idempotency-Key", process.env.RUN_KEY!),
HttpClientRequest.bodyJsonUnsafe({ model: "wan-3.0", prompt: "a lighthouse at dusk", duration: 5, resolution: "480p" }),
);
const job: any = yield* (yield* http.execute(submit)).json;
while (true) {
yield* Effect.sleep("30 seconds");
const s: any = yield* (yield* http.execute(HttpClientRequest.get(job.polling_url))).json;
if (s.status === "completed") return s.unsigned_urls[0] as string;
if (s.status === "failed" || s.status === "cancelled") return yield* Effect.fail(`job ${s.status}`);
}
});
Effect.runPromise(program.pipe(Effect.provide(FetchHttpClient.layer))).then(console.log, console.error);What does it do on a 402 or a failed job?
A 402 insufficient_credits is not in the transient list, so filterStatusOk fails the effect at once and nothing is retried. That is what you want: credits do not return by waiting. A job that reaches failed or cancelled ends the loop with Effect.fail. The poll interval is 30 seconds, the pace the video guide suggests, because a clip takes from 30 seconds to several minutes.
What should you still decide yourself?
Choose times. Three retries keep a stuck request from running long, and the key makes each one safe. Generate RUN_KEY once per logical clip, store it before the first request, and reuse it after a restart: a replay returns the original job. Retries do not cover the poll loop. If your process dies mid-wait, resume by reading the job id you stored.
Sources
Related posts
More in Developers
- Gemini Omni edit returns 400: the five field mistakes on Sume
A video_url edit on gemini-omni-flash-1.1 fails with 400 when you add aspect_ratio, generate_audio false, bitrate_mode, reference audio, or other media. Fixes.
- Generate 100 images with the Sume Image API: async jobs and polling
Submit each image with mode async, collect job ids, then poll status. Each job bills only if it completes; failed jobs use no credits.
- Generate Grok Imagine clips from an agent with the generate_video tool
Hosted MCP exposes generate_video, which replaced video-router_create. How an agent should call it for grok-imagine-video-1.5 and what to read first.
- Can the Sume Image API output SVG? Not today; formats you get
No: output_format svg is in the schema for parity, but no catalog model advertises it, so the request is rejected. Sume returns png, jpeg or webp.
Written by Sume