A for await loop over Sume job status: one async generator, 3 runtimes
Wrap Sume status polling in an async generator and read it with for await. One fetch-only file ran unchanged on Node, Bun and Deno and honors the poll hint.

Write async function* pollJob(id) that fetches /v1/jobs/{id}/status, yields every snapshot, and sleeps next_poll_after_seconds between reads; callers then write for await (const s of pollJob(id)). The file uses only fetch and setTimeout, so the same code ran unchanged on Node, Bun and Deno when I tested it against a local stand-in for the status route.
A generator is a better shape than a callback or a helper that returns only the final body. The caller decides what to do with each snapshot, such as updating a progress bar or logging queue.state, and break stops the loop and the polling with it.
The generator
Each pass reads status, throws on a non-2xx response with the body attached, yields the data object, and returns when terminal is true. The cap on polls protects a CI job from waiting forever. The hint comes from the Jobs and results page, which says to obey next_poll_after_seconds when present; 2 seconds is this sample's fallback.
const base = "https://api.sume.com";
const headers = { "x-api-key": process.env.SUME_API_KEY };
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));
export async function* pollJob(id, { maxPolls = 60 } = {}) {
for (let i = 0; i < maxPolls; i++) {
const res = await fetch(`${base}/v1/jobs/${id}/status`, { headers });
if (!res.ok) throw new Error(`status ${res.status}: ${await res.text()}`);
const { data } = await res.json();
yield data; // caller sees every snapshot
if (data.terminal) return;
await sleep(data.next_poll_after_seconds ?? 2);
}
throw new Error(`job ${id} not terminal after ${maxPolls} polls`);
}
if (process.argv[2]) {
for await (const s of pollJob(process.argv[2])) {
console.log(s.sume_status, s.queue?.state ?? "-");
}
}Why yield before checking terminal
The generator yields the terminal snapshot too, so the last iteration of the caller's loop sees the finished state and can read result_ready. Returning before the yield would hide the one snapshot you most need.
Do not treat the status word as the finish line on its own. The docs expose terminal and result_ready as separate fields, and the generator stops on terminal because that is the field defined for that purpose. After the loop, fetch /v1/jobs/{id}/result once.
What differs per runtime
Nothing in the code differs, but the way you run it does. These commands are what I used, with the key in the environment.
| Runtime | Command | Notes |
|---|---|---|
| Node 22 | node poll.mjs job_123 | fetch and top-level for await are built in, no flags |
| Bun | bun poll.mjs job_123 | Same file, same output |
| Deno | deno run -A poll.mjs job_123 | Needs network and env permission, -A is the short way |
What this is not
It is polling, not streaming. Sume has no SSE or WebSocket endpoint for job progress; the status route is a pull, and /events is a snapshot you read, not a stream you subscribe to. The generator gives your code a streaming-looking surface over those pulls. If you need to push those updates to a browser, put your own server-sent events endpoint in front of this loop, as in this post.
Mind the read budget while you do. Reads have their own per-minute bucket, forty times the write number for your plan, so a handful of generators polling at the hinted interval is far below it. A hundred generators on a 1-second loop is not, so keep the hint.
Sources
Related posts
More in Developers
- Generate a Go client from the Sume OpenAPI with oapi-codegen
The full Sume spec trips oapi-codegen on a [number, null] type, but filtering to the operations you use works. Config, generated names and a short caller.
- GitHub Actions concurrency for a Sume render job: the cancel trap
cancel-in-progress stops your workflow, not the Sume job it submitted. Group by branch, cancel via POST /v1/jobs/{id}/cancel in a final step, and cap the run.
- Go 1.27.2 and 1.26.9 patch net/http: update your Sume webhook receiver
Go 1.27.2 and 1.26.9 (2026-10-08) list security fixes in net/http and crypto/tls. A stdlib receiver for Sume webhooks that verifies the signature, in two files.
- Go errgroup SetLimit: submit Sume jobs within your concurrency budget
Cap in-flight Sume submissions in Go with errgroup.SetLimit, size the width from your concurrency limit, and keep one Idempotency-Key per item. 30 lines.
Written by Sume