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

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.

```elixir
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:

```elixir
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](https://hexdocs.pm/oban/Oban.Worker.html#module-snoozing-jobs)


---

Created by: almirsarajcic
Date: March 25, 2026
URL: https://elixirdrops.net/d/QiNjMD2p
