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.

5 min readSume
All posts

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
end

Why does each branch return what it does?

Oban return values for each Sume poll outcome, read 2026-10-04
Sume answerOban returnReason
200, terminal, completedstore result, :okFetch /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

All Developers posts

Written by Sume