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
- Which fps to set: TikTok 23-60, YouTube list, Sume's 24/25/30/60
Sume Timeline fps can be 24, 25, 30 or 60, and every one is in TikTok's 23 to 60 window and on YouTube's list. Leave fps unset to match your sources.
- 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.
Written by Sume