60 video jobs in Node with a promise pool: which plans need waves
A Node promise pool for Sume video jobs, plus the fit table: 60 jobs against accepted capacity on Free, Pro, Startup and Scale, and how many waves each needs.

Sixty jobs fit in a single burst only on Scale, whose accepted capacity is 120. Startup accepts 48, Pro 24 and Free 6, so on those plans a 60-job batch must go out in waves or it meets 429 queue_full. A promise pool with a width equal to the processing cap is the Node way to do that.
The cost is the same on every plan: sixty 10-second Wan 3.0 clips at 480p total $37.50 on Sume.
Fit table
Waves means the number of rounds if you only keep one processing width in flight.
| Plan | Width | Accepted capacity | 60 in one burst? | Waves at width |
|---|---|---|---|---|
| Free | 1 | 6 | No, queue_full | 60 |
| Pro | 4 | 24 | No, queue_full | 15 |
| Startup | 8 | 48 | No, queue_full | 8 |
| Scale | 20 | 120 | Yes | 3 |
The pool
Workers pull the next index until the list is empty, which keeps exactly width jobs open.
const API = "https://api.sume.com";
const H = { Authorization: "Bearer " + process.env.SUME_API_KEY, "Content-Type": "application/json" }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function one(i, prompt) {
const res = await fetch(API + "/v1/videos", {
method: "POST",
headers: { ...H, "Idempotency-Key": "pool-" + String(i).padStart(3, "0") },
body: JSON.stringify({ model: "wan-3.0", prompt, duration: 10, resolution: "480p" }),
});
if (!res.ok) throw new Error("submit " + i + " -> " + res.status);
const { id } = await res.json();
for (;;) {
const r = await fetch(API + "/v1/jobs/" + id + "/status", { headers: H });
const s = (await r.json()).data;
if (s.terminal) return { i, id, status: s.sume_status };
await sleep((s.next_poll_after_seconds ?? 5) * 1000);
}
}
async function pool(items, width, fn) {
const out = [];
let next = 0;
const worker = async () => {
while (next < items.length) { const i = next++; out[i] = await fn(i, items[i]); }
};
await Promise.all(Array.from({ length: width }, worker));
return out;
}
const prompts = Array.from({ length: 60 }, (_, n) => "Lantern festival, shot " + n);
console.log(await pool(prompts, 8, one));Notes
The width of 8 is Startup's processing cap. Replace it with concurrency_limit from your own generation_limits. Run the file as an ES module on Node 18 or later.
Why a pool and not Promise.all
Promise.all over 60 submits would send all 60 at once. On a plan with less accepted capacity, the tail of the burst gets 429 queue_full. A pool of fixed width keeps the number of open jobs near the processing cap, so queue slots stay free and the error never appears.
- Width 1 to 4 suits Free and Pro; 8 suits Startup; 20 suits Scale.
- Prefer the effective
concurrency_limitfromgeneration_limitsover the table value.
The pool returns results in input order because each worker writes to out[i], so you can zip the output back onto your source rows without sorting.
Sources
Related posts
More in Developers
- Sora content rules vs Sume generation_rejected: read the job events
A prompt Sora refused may behave differently on Sume. How a rejection shows up as generation_rejected, which events to read, and why you must not assume parity.
- Sora download_content vs Sume /content: the 302 and two 409 errors
OpenAI's content endpoint took variant=video, thumbnail or spritesheet. Sume's /content redirects with a 302 and returns two different 409s. How to branch.
- Sora input_reference image: upload with uploadFile, use frame_images
OpenAI took the first-frame image as multipart input_reference. Sume wants an HTTPS URL: upload with the SDK's uploadFile, then pass it as frame_images.
- Sora jobs in flight at shutdown: reconcile, then resubmit to Sume
OpenAI's page gives the removal date but not in-flight jobs. Mark every unfinished Sora row, then resubmit its prompt to Sume with a key built from the old id.
Written by Sume