Quick start
From a plain Sidekiq worker to a deduplicated one in five minutes — write it, watch a duplicate get dropped, then change what happens instead.
Before you start#
This page assumes the middleware is already wired into your Sidekiq initializer. If you haven't done that yet, it's a one-time copy-paste — see Installation. Nothing on this page works until the client and server middleware are registered.
1. Write a unique worker#
One line of sidekiq_options opts a worker into uniqueness.
Take an ordinary worker and add a lock:. We'll use :until_executed, which holds the lock from the moment the job is enqueued until perform finishes — so no duplicate can slip in for the entire lifecycle of the job.
class SendWelcomeEmailJob
include Sidekiq::Job
sidekiq_options lock: :until_executed
def perform(user_id)
user = User.find(user_id)
WelcomeMailer.welcome(user).deliver_now
end
endThat's the whole change. Uniqueness is keyed on the worker class, the queue, and the arguments — so SendWelcomeEmailJob.perform_async(1) and SendWelcomeEmailJob.perform_async(2) are two independent jobs, while a second perform_async(1) fired before the first has run is a duplicate.
2. Enqueue it twice#
The second push, while the first is locked, is dropped.
Enqueue the same arguments twice in a row. The first call acquires the lock and returns a job ID; the second finds the lock already held and is discarded, returning nil.
SendWelcomeEmailJob.perform_async(1) # => "a1b2c3..." enqueued, lock acquired
SendWelcomeEmailJob.perform_async(1) # => nil duplicate, dropped
SendWelcomeEmailJob.perform_async(2) # => "d4e5f6..." different args, enqueuedDropping the duplicate is the default because no on_conflict strategy was set — the conflict is logged and the extra job is discarded. Once the first job runs to completion the lock is released, and perform_async(1) will enqueue normally again.
3. Change what happens on a conflict#
Reschedule the duplicate instead of dropping it.
Dropping is not always what you want. Add on_conflict: to choose a different outcome. With :reschedule, the duplicate isn't thrown away — it's re-enqueued to run later, so it still happens once the lock clears.
class SendWelcomeEmailJob
include Sidekiq::Job
sidekiq_options lock: :until_executed, on_conflict: :reschedule
def perform(user_id)
user = User.find(user_id)
WelcomeMailer.welcome(user).deliver_now
end
endNow the second perform_async(1) is re-enqueued to run later rather than dropped. The other strategies cover the rest of the spectrum: :raise makes Sidekiq retry the job, :reject sends the duplicate to the Dead set, and :replace deletes the original and enqueues the new one. See Conflict resolution for the full table.
Where to go next#
- Choosing a lock type — a decision table from "I want to prevent X" to the lock that does it.
:until_executedis one of five. - Use cases — start with debouncing duplicate enqueues; each use-case page is a complete, working worker you can lift straight into your app.