Node 22.23.3: pace Sume status polls with ratelimit headers

Read ratelimit-remaining, ratelimit-reset and retry-after from every Sume response in Node 22.23.3 and slow a wave of job polls before a 429 appears.

5 min readSume
All posts

Short answer

Read ratelimit-remaining and ratelimit-reset on every Sume response, sleep for retry-after on a 429, and slow down when the remaining count gets low. Sume's authentication page says to read the header rather than count requests yourself, because the headers describe whichever budget the current request spent from.

In Node 22.23.3, which the release notes date to 23 September 2026 and which bundles Undici 6.28.1, fetch exposes those headers through res.headers.get(...) with no extra library.

The budgets

Reads and writes have separate budgets, so a tight status-poll loop cannot 429 your own submits. A read is any GET or HEAD; the plan number is the write number, and reads get forty times that in their own bucket.

Sume per-minute budgets by plan (as of 2026-10-03)
PlanWrites per minuteReads per minute
Free1204800
Pro30012000
Startup60024000
Scale120048000

What the headers mean

A 429 names the exhausted budget in error.details.scope, either read or write. A 429 with code queue_full is a different thing: workspace generation concurrency plus queue capacity is full, which a faster or slower poll does not change.

Rate-limit response headers (as of 2026-10-03)
HeaderMeaning
ratelimit-limitrequests allowed in the current window
ratelimit-remainingrequests left in the current window
ratelimit-resetseconds until the window resets
retry-afterseconds to wait, sent on 429

A paced wave poller

Run node wave.mjs job_a job_b job_c. It reads each unfinished job once per round, sleeps two seconds between rounds, and sleeps until the window resets if fewer than 20 requests remain. The 20 is a safety margin you can tune.

const key = process.env.SUME_API_KEY;
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));

async function read(id) {
  for (;;) {
    const res = await fetch(`https://api.sume.com/v1/jobs/${id}/status`, {
      headers: { Authorization: `Bearer ${key}` },
    });
    if (res.status === 429) { await sleep(Number(res.headers.get("retry-after")) || 1); continue; }
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    if (Number(res.headers.get("ratelimit-remaining")) < 20) {
      await sleep(Number(res.headers.get("ratelimit-reset")) || 1);
    }
    return (await res.json()).data;
  }
}

const ids = process.argv.slice(2);
const done = new Map();
while (done.size < ids.length) {
  for (const id of ids.filter((i) => !done.has(i))) {
    const s = await read(id);
    if (s.terminal) done.set(id, s.sume_status);
  }
  await sleep(2);
}
console.log(Object.fromEntries(done));

What Sume does and does not do

Sume gives polls their own, larger budget and reports it in headers. Over MCP, a jobs_status poll spends no write budget at all, and a tool call spends the write budget once for the run it creates.

Sume does not raise generation concurrency when you raise your request rate; that is governed by the plan's concurrency limit, reported on the generation_limits object. Unauthenticated requests are limited per client IP, with a read bucket of four times the write rate, so always send the key.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume