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.

5 min readSume
All posts

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.

Transient statuses versus what a Sume submit should do (read 2026-10-13)
StatusretryTransientSume submit guidance
429Retries, waits for Retry-AfterRetry with the same Idempotency-Key
500, 502, 503, 504RetriesRetry only with the same Idempotency-Key
408 or a dropped connectionRetriesThe job may exist: the key makes the replay safe
402Not retriedStop: 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

All Developers posts

Written by Sume