Poll for scene and render results

Scene analysis and renders finish in the background. Poll them on a schedule that is quick to finish and gentle on your rate limit.

Updated

What runs in the background

You sendIt answersThen pollUntil status is
POST /scenes201, analyzingGET /scenes/{id}ready or failed
POST /renders202, queuedGET /renders/{id}completed, failed or cancelled

Both always reach one of their final statuses, so a poll that waits for them always ends. Keep a deadline of your own too, so a network fault cannot leave your job waiting forever.

A polling schedule

  • Wait about 5 seconds before the first check.
  • Then check every few seconds, growing the wait a little each time, up to about 15 seconds between checks.
  • Set a deadline. A scene or a single render is normally final within a few minutes. Renders run 3 at a time per workspace, so for a large batch allow half an hour or more.

That keeps a typical render to a handful of requests, far inside your rate limit of 600 a minute.

Wait for a scene

const BASE = "https://www.skipshoot.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.SKIPSHOOT_SECRET_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// A 429 or a 5xx is worth waiting out: null, after any Retry-After. Any
// other error is a mistake to fix, so it throws.
async function get(path) {
  const response = await fetch(`${BASE}${path}`, { headers });

  if (response.ok) {
    return response.json();
  }

  if (response.status === 429 || response.status >= 500) {
    await sleep(Number(response.headers.get("Retry-After") ?? 0) * 1000);

    return null;
  }

  const { error } = await response.json();
  throw new Error(`${response.status} ${error.type}: ${error.message}`);
}

async function waitForScene(id, { timeoutMs = 10 * 60_000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  let wait = 5_000;

  while (Date.now() < deadline) {
    await sleep(wait);
    const scene = (await get(`/scenes/${id}`))?.scene;

    if (scene?.status === "ready" || scene?.status === "failed") {
      return scene;
    }

    wait = Math.min(wait * 1.5, 15_000);
  }

  throw new Error(`Scene ${id} was not ready in time`);
}

Wait for renders

const FINAL = new Set(["completed", "failed", "cancelled"]);

async function waitForRender(id, { timeoutMs = 30 * 60_000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  let wait = 5_000;

  while (Date.now() < deadline) {
    await sleep(wait);
    const render = (await get(`/renders/${id}`))?.render;

    if (render && FINAL.has(render.status)) {
      return render;
    }

    wait = Math.min(wait * 1.5, 15_000);
  }

  throw new Error(`Render ${id} did not finish in time`);
}

Poll many renders efficiently

After a batch, poll only the renders still unfinished in each round, and drop each one as it reaches a final status. Renders start in the order you sent them, so poll in that order too: the first ones finish first.

async function collect(ids, onDone) {
  const open = new Set(ids);

  while (open.size > 0) {
    await sleep(10_000);

    for (const id of [...open].slice(0, 20)) {
      const render = (await get(`/renders/${id}`))?.render;

      if (render && FINAL.has(render.status)) {
        open.delete(id);
        await onDone(render);
      }
    }
  }
}

Checking the 20 oldest unfinished renders every 10 seconds is about 120 requests a minute at most, and far fewer as renders finish.

Poll from a scheduled job

For large catalogs, store each render’s id when you queue it, then run a job every minute or so that fetches the unfinished ones, saves the finished images and marks them done. A crash or deploy loses nothing, because the ids are stored and every fetch is safe to repeat.

Webhooks

The API does not send webhooks yet. Polling, as above, is how you learn that a scene is ready or a render has finished.

Something these pages do not answer? Email admin@skipshoot.com and we’ll help.