Oban worker that polls a Sume job: snooze instead of sleeping
Submit a Sume job once, then let an Oban worker check status, return {:snooze, seconds} while it runs and finish on terminal. Elixir with Req, no sleep loop.

In Elixir, poll a Sume job with an Oban worker that reads /v1/jobs/{id}/status once per run and returns {:snooze, seconds} while terminal is false. Oban's worker docs describe {:snooze, period} as rescheduling the job to run again after the period without counting as a failed attempt (read 2026-10-04). That keeps a worker process free between polls, instead of a Process.sleep loop holding a queue slot for minutes.
The Sume side comes from Jobs and results: the status read carries terminal, sume_status and next_poll_after_seconds, which is null once the job is terminal.
What does the worker look like?
Insert it with the Sume job id in args and a unique window so a double insert does not start two pollers.
defmodule MyApp.PollSumeJob do
use Oban.Worker, queue: :media, max_attempts: 5,
unique: [period: 3600, keys: [:job_id]]
@impl true
def perform(%Oban.Job{args: %{"job_id" => id}}) do
resp =
Req.get!("https://api.sume.com/v1/jobs/#{id}/status",
headers: [{"x-api-key", System.fetch_env!("SUME_API_KEY")}],
retry: false, receive_timeout: 30_000)
case {resp.status, resp.body} do
{200, %{"data" => %{"terminal" => true, "sume_status" => "completed"}}} ->
MyApp.Media.store_result(id)
{200, %{"data" => %{"terminal" => true, "sume_status" => s}}} ->
{:cancel, "job #{id} ended #{s}"}
{200, %{"data" => d}} ->
{:snooze, max(d["next_poll_after_seconds"] || 5, 5)}
{429, _} ->
{:snooze, String.to_integer(Req.Response.get_header(resp, "retry-after") |> List.first("30"))}
{status, _} when status >= 500 -> {:error, "sume #{status}"}
{status, _} -> {:cancel, "sume #{status}"}
end
end
endWhy does each branch return what it does?
| Sume answer | Oban return | Reason |
|---|---|---|
| 200, terminal, completed | store result, :ok | Fetch /result or the job record and persist artifact URLs |
| 200, terminal, failed or canceled | {:cancel, reason} | Retrying a finished job cannot change it |
| 200, still running | {:snooze, n} | Honors next_poll_after_seconds and reschedules without failing the job |
| 429 | {:snooze, retry-after} | A read-budget limit is transient; the job keeps running |
| 5xx | {:error, reason} | Oban backoff retries; counts as an attempt |
| Other 4xx | {:cancel, reason} | Fix the request or the key; do not loop |
How many polls is that?
Polling spends your read budget, and a long video job can take minutes, so respect next_poll_after_seconds and floor it. A webhook avoids the loop entirely for long jobs; see Webhooks. Check how your Oban version counts snoozes against max_attempts before you rely on a long poll chain.
What about the submit step?
Submit in a separate worker or in the code that enqueues this one, with an Idempotency-Key derived from your own record id, so a retried submit returns the same job id and does not bill twice.
Sources
Related posts
More in Developers
- One Gemini billing cap pauses every linked project: guard the batch
Google pauses all projects on a billing account at the tier cap. Add a balance check and idempotent submits so one Omni batch cannot stall the rest.
- One TypeScript job shape for Sume /v1/videos and /v1/jobs responses
Sume's /v1/videos returns a bare object and /v1/jobs wraps in data. A short normalizer maps both to one state, including the cancelled versus canceled spelling.
- OpenAI Agents API hosted sandbox: which Sume hosts to allow
OpenAI's Agents API is in public beta with hosted or connected sandboxes. Which Sume hosts to allow, how to pass the MCP URL, and why the key stays in a secret.
- OpenAI Agents API sandbox: keep the Sume API key out of it
The OpenAI Agents API beta adds a sandbox and hosted-browser computer use. Where a Sume API key can live when an agent runs there, and what to hand it instead.
Written by Sume