We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Prevent duplicate Oban jobs with `unique` worker options
almirsarajcic
Multiple button clicks or retries can enqueue the same background job multiple times, causing duplicate charges, emails, or API calls. Oban’s built-in uniqueness prevents this without custom deduplication logic.
# ❌ WITHOUT uniqueness - Same job runs multiple times
defmodule MyApp.ChargeCustomerWorker do
use Oban.Worker
@impl Oban.Worker
def perform(%Oban.Job{args: %{"customer_id" => customer_id}}) do
charge_customer(customer_id) # Charges 3 times if enqueued 3 times!
{:ok, "Charged customer #{customer_id}"}
end
end
# ✅ WITH uniqueness - Only first job runs within time window
defmodule MyApp.ChargeCustomerWorker do
use Oban.Worker, unique: [period: 60]
@impl Oban.Worker
def perform(%Oban.Job{args: %{"customer_id" => customer_id}}) do
charge_customer(customer_id) # Runs once, duplicates discarded
{:ok, "Charged customer #{customer_id}"}
end
end
The unique: [period: 60]
option tells Oban: “If a job with identical args already exists and was inserted in the last 60 seconds, discard this duplicate.” The period is in seconds.
How uniqueness works
When you enqueue multiple identical jobs quickly:
# Enqueue same job 3 times within 1 second
MyApp.ChargeCustomerWorker.new(%{customer_id: 123}) |> Oban.insert() # ✅ Inserted
MyApp.ChargeCustomerWorker.new(%{customer_id: 123}) |> Oban.insert() # ❌ Discarded
MyApp.ChargeCustomerWorker.new(%{customer_id: 123}) |> Oban.insert() # ❌ Discarded
Only the first job gets inserted. The duplicates return {:ok, :discard}
instead of creating new jobs.
Advanced uniqueness configuration
Fine-tune uniqueness checking with additional options:
defmodule MyApp.NotificationWorker do
use Oban.Worker,
unique: [
period: 300, # 5 minute window
fields: [:args, :worker], # Match on args and worker type
keys: [:user_id, :notification_type], # Only these arg keys matter
states: [:available, :scheduled, :executing] # Check these job states
]
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id, "notification_type" => type}}) do
send_notification(user_id, type)
{:ok, "Sent #{type} notification to user #{user_id}"}
end
end
Configuration breakdown:
period: 300
- Check for duplicates in the last 5 minutesfields: [:args, :worker]
- Match based on job arguments and worker module (default)keys: [:user_id, :notification_type]
- Only compare these specific arg keys (ignore other args)states: [:available, :scheduled, :executing]
- Check jobs in these states (excludes:completed
)
Common use cases
Payment processing:
use Oban.Worker, unique: [period: 300] # Prevent double charges within 5 minutes
Email notifications:
use Oban.Worker, unique: [period: 3600] # One notification per hour max
API rate limiting:
use Oban.Worker, unique: [period: 60, keys: [:user_id]] # One API call per user per minute
Pro tip: Set period
based on how long duplicate jobs should be prevented. For payment processing, use shorter windows (60-300 seconds). For daily reports, use longer windows (86400 seconds = 24 hours).
copied to clipboard