Pause an Oban job with `{:snooze, seconds}`

almirsarajcic

almirsarajcic

1 hour ago

0 comments

Sometimes a job isn’t ready to run yet — a third-party API is rate-limiting you, a resource hasn’t been created, or you just need to wait a few minutes before retrying. Returning {:snooze, seconds} from perform/1 delays the job without counting it as a failure.

defmodule MyApp.Workers.SyncWorker do
  use Oban.Worker, queue: :default, max_attempts: 5

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
    case MyApp.API.fetch_user(user_id) do
      {:ok, user} ->
        MyApp.Sync.run(user)

      {:error, :rate_limited} ->
        {:snooze, 60}

      {:error, reason} ->
        {:error, reason}
    end
  end
end

The job is rescheduled seconds from now and marked as snoozed in the database — not retryable, not discarded. It won’t appear as a failure in your error tracker and won’t count toward max_attempts.

The seconds value must be a non-negative integer. Snoozing indefinitely with large values is valid but consider :cancel if the job should never run again.

All perform/1 return values:

:ok / {:ok, value}   # job succeeds
{:snooze, seconds}   # delayed, no failure recorded
{:error, reason}     # failure, counts toward max_attempts
{:cancel, reason}    # stopped permanently, no more retries

Note: :discard and {:discard, reason} are deprecated — use {:cancel, reason} instead.

OSS caveat: attempt counter increments on snooze

In Oban OSS, snoozing increments both attempt and max_attempts by one to preserve the remaining retry budget. This means job.attempt and job.max_attempts are both higher than you’d expect after a snooze. If your backoff/1 callback or business logic reads job.attempt, you need to account for this:

defmodule MyApp.Workers.SyncWorker do
  use Oban.Worker, queue: :default, max_attempts: 5

  @max_attempts 5

  @impl Oban.Worker
  def backoff(%Oban.Job{} = job) do
    corrected_attempt = @max_attempts - (job.max_attempts - job.attempt)
    trunc(:math.pow(corrected_attempt, 4) + 15 + :rand.uniform(30) * corrected_attempt)
  end

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
    case MyApp.API.fetch_user(user_id) do
      {:ok, user} -> MyApp.Sync.run(user)
      {:error, :rate_limited} -> {:snooze, 60}
      {:error, reason} -> {:error, reason}
    end
  end
end

Oban Pro handles this cleanly without the attempt inflation — if you’re on Pro, snoozing just works as you’d expect.

Oban docs: Snoozing jobs

Comments (0)

Sign in with GitHub to join the discussion