Getting started

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
end

That'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, enqueued

Dropping 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.

Uniqueness is enforced even in tests. Sidekiq::Testing.inline! runs jobs immediately with locking still in effect, so you can drive this exact behavior in a console or spec.

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
end

Now 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_executed is 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.