Poll /v1/jobs/{id}/status, then /result, in Node with backoff

A 25-line Node function that polls a Sume job on its own next_poll_after_seconds, reads /result only when the job completed, and keeps the id on timeout.

3 min readSume
All posts

Poll GET /v1/jobs/{id}/status until terminal is true, then call GET /v1/jobs/{id}/result once, and only when the status is completed. The status payload tells you how long to wait in next_poll_after_seconds; a fixed sleep ignores that hint, and a result call on an unfinished job returns 409 job_not_completed.

Jobs and results documents the same loop in pseudocode. Below is a working version for Node 18 and later.

The function

Every public job read wraps its object in data. The id comes from the submit response of /v1/videos (id).

const API = "https://api.sume.com";
const H = { Authorization: "Bearer " + process.env.SUME_API_KEY };
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));

async function get(path) {
  const res = await fetch(API + path, { headers: H });
  if (!res.ok) throw new Error(path + " -> " + res.status);
  return (await res.json()).data;
}

export async function waitForResult(jobId, deadlineMs = 20 * 60_000) {
  const stop = Date.now() + deadlineMs;
  let backoff = 2;
  while (Date.now() < stop) {
    const s = await get("/v1/jobs/" + jobId + "/status");
    if (s.terminal) {
      if (s.sume_status !== "completed") throw new Error("job " + s.sume_status);
      return (await get("/v1/jobs/" + jobId + "/result")).result.artifacts;
    }
    await sleep(s.next_poll_after_seconds ?? backoff);
    backoff = Math.min(backoff * 2, 30);
  }
  throw new Error("deadline reached; job " + jobId + " still runs and still bills");
}

Why it is built this way

  • terminal and result_ready are booleans on the status payload; sume_status carries the lowercase state.
  • A client deadline stops your wait, not the job. The error message keeps the id so you can read it again or cancel it.
  • queued is a normal state: with the workspace at its concurrency cap, accepted jobs wait for a seat.
  • Failed and canceled jobs answer /result with 409 job_not_completed, so read the failure from GET /v1/jobs/{id} instead.

Where this fits

Polling is the fallback even when you use webhooks. A delivery that exhausts its attempts does not change the job's real state, and the status URL still has the answer.

Adapting it

The function returns the artifact list from the result. Each artifact has an id, a url under media.sume.com, a type and a content_type, so a caller can pick the first video entry and stream it. Store the Sume URL; raw provider URLs are not part of the public contract.

  • Add an AbortSignal parameter if your worker can be shut down mid-wait, and pass it to fetch.
  • Add jitter to the sleep when you run many pollers in one process, so they do not hit the read limit as a group.
  • Treat a 429 on a status read as a reason to wait longer, not as a job failure; the job is unaffected.
  • Log the job id on every poll error so a restart can resume by reading the same id.

The 20-minute default is a client choice, not a Sume limit. The docs call a deadline of about that length reasonable for video, and the deadline lives in your client precisely because no HTTP request should stay open that long.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume