Node fetch worker pool: submit transcription jobs with retry-after
A 30-line Node 18+ worker pool that posts Sume STT jobs, sleeps for retry-after on 429, and sends an Idempotency-Key per clip. Tested against a stub.

In Node 18 or later, submit Sume transcription jobs with the built-in fetch and a small worker pool: N workers pull clips from a shared index, post to /v1/stt-1.0/transcribe, and on 429 sleep for the retry-after header before trying again with the same Idempotency-Key. Save the file as .mjs so top-level await works. The 429 and idempotency rules come from Errors and rate limits, read 2026-10-06.
Why a pool and not Promise.all
Promise.all over 10,000 clips opens 10,000 requests at once. A pool of 3 to 8 workers keeps the request rate steady, so you reach 429 rate_limited rarely, and 429 queue_full only when your plan's accepted-job capacity is truly full.
| Setting | Start with | Reason |
|---|---|---|
| Workers | 3 to 8 | Steady request rate; submits are cheap |
| Attempts per clip | 5 | Then log the clip and move on |
| Key | One per clip, stable across retries | A retry returns the original job |
duration_seconds | Always send it | Holds seconds, not the one-minute default |
The pool
Run against a local stub that answered one 429 with retry-after: 1, it printed a job id for each of three clips.
const API = process.env.SUME_API ?? "https://api.sume.com";
const auth = { Authorization: `Bearer ${process.env.SUME_API_KEY}` };
async function submit([url, seconds, key]) {
for (let i = 0; i < 5; i++) {
const res = await fetch(`${API}/v1/stt-1.0/transcribe`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify({ audio_url: url, duration_seconds: seconds }),
});
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after") ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return [key, (await res.json()).request_id];
}
return [key, null];
}
async function pool(items, limit) {
const out = [];
let next = 0;
const worker = async () => {
while (next < items.length) out.push(await submit(items[next++]));
};
await Promise.all(Array.from({ length: limit }, worker));
return out;
}
const clips = [1, 2, 3].map((i) => [`https://media.sume.com/c${i}.wav`, 5, `stt-${i}`]);
console.log(await pool(clips, 2));Sources
Related posts
More in Developers
- Nova Canvas boto3 read timeout vs Sume's 30 second wait and 202
The AWS SDK read timeout is 60 s and Amazon suggests 300 s for Nova Canvas. Sume's /v1/images waits 30 s, then returns a 202 job. Handle both in Python.
- Nova Canvas cfgScale 1.1 to 10 vs Sume: no guidance field
Nova Canvas has a cfgScale from 1.1 to 10, default 6.5. Sume rejects fields a model does not list. How to get the same effect with wording and model choice.
- Nova Canvas returned fewer images than asked: Sume n and data length
Nova Canvas can return fewer images than numberOfImages when moderation blocks some. How to count what you got and what you paid for on Sume.
- Nova Canvas rejects PNGs with transparent pixels: flatten before edits
Nova Canvas needs 8-bit PNG or JPEG with no transparent pixels in the alpha channel. Flatten cutouts onto white before any image edit, here and on Sume.
Written by Sume