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.

4 min readSume
All posts

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.

Pool settings to start with. Limits come from Generation admission (docs.sume.com), read 2026-10-06.
SettingStart withReason
Workers3 to 8Steady request rate; submits are cheap
Attempts per clip5Then log the clip and move on
KeyOne per clip, stable across retriesA retry returns the original job
duration_secondsAlways send itHolds 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

All Developers posts

Written by Sume