A long-lived project that still receives updates
Prevents simultaneous Sidekiq jobs with the same unique arguments to run. Highly configurable to suite your specific needs.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 1.0, >= 1.0.5
>= 7.0.0, < 9.0.0
>= 1.0, < 3.0
 Project Readme

sidekiq-unique-jobs

Prevents duplicate Sidekiq jobs. Uses Redis locks to ensure only one job with the same arguments runs at a time.

Build Status

Support Me

Want to show me some love for the hard work I do on this gem? You can use the following PayPal link: https://paypal.me/mhenrixon2. Any amount is welcome and let me tell you it feels good to be appreciated. Even a dollar makes me super excited about all of this.

Requirements

  • Ruby >= 3.2
  • Sidekiq >= 8.0
  • Redis >= 6.2 (for LMOVE support)

Installation

gem "sidekiq-unique-jobs", "~> 9.0"

Quick Start

# config/initializers/sidekiq.rb
Sidekiq.configure_client do |config|
  config.client_middleware do |chain|
    chain.add SidekiqUniqueJobs::Middleware::Client
  end
end

Sidekiq.configure_server do |config|
  config.client_middleware do |chain|
    chain.add SidekiqUniqueJobs::Middleware::Client
  end

  config.server_middleware do |chain|
    chain.add SidekiqUniqueJobs::Middleware::Server
  end

  SidekiqUniqueJobs::Server.configure(config)
end
class MyJob
  include Sidekiq::Job

  sidekiq_options lock: :until_executed

  def perform(user_id)
    # Only one job per user_id runs at a time
  end
end

That's it. Duplicate jobs are silently dropped by default.

Lock Types

Type Locks at Unlocks at Use case
:until_executing Enqueue Before perform Prevent duplicate enqueuing
:until_executed Enqueue After perform Prevent duplicates until job completes
:until_expired Enqueue TTL expiry Time-based uniqueness (e.g. daily jobs)
:while_executing Perform After perform Prevent concurrent execution
:until_and_while_executing Enqueue + Perform Before + After perform Full lifecycle protection

Conflict Strategies

When a duplicate is detected, the conflict strategy determines what happens:

Strategy Behavior
:log (default) Log and discard the duplicate
:raise Raise SidekiqUniqueJobs::OnConflict::Raise
:reject Send to dead set
:replace Delete the existing job and enqueue the new one
:reschedule Schedule the duplicate to run later
sidekiq_options lock: :until_executed,
               on_conflict: :reject

# Or different strategies for client (enqueue) and server (execute):
sidekiq_options lock: :until_and_while_executing,
               on_conflict: { client: :log, server: :reschedule }

Configuration

SidekiqUniqueJobs.configure do |config|
  config.lock_ttl          = nil     # Lock expiration in seconds (nil = no expiry)
  config.lock_timeout      = 0       # How long to wait for a lock (0 = don't wait)
  config.lock_prefix       = "uniquejobs"
  config.on_conflict       = nil     # Global default conflict strategy
  config.lock_info         = false   # Store lock metadata (useful for debugging)
  config.enabled           = true    # Disable uniqueness globally
  config.reaper            = :ruby   # Orphaned lock cleanup (:ruby, :lua, true, :none, false)
  config.reaper_count      = 1000    # Max locks to reap per cycle
  config.reaper_interval   = 600     # Seconds between reaper runs
  config.reaper_timeout    = 10      # Max seconds per reaper run
  config.digest_algorithm  = :legacy # :legacy (MD5) or :modern (SHA3-256)
end

Controlling Uniqueness

Custom Lock Arguments

By default, uniqueness is based on worker class, queue, and all arguments. To customize:

class MyJob
  include Sidekiq::Job

  sidekiq_options lock: :until_executed,
                 lock_args_method: ->(args) { [args.first] }

  def perform(user_id, timestamp)
    # Only user_id determines uniqueness, timestamp is ignored
  end
end

Lock TTL

sidekiq_options lock: :until_expired,
               lock_ttl: 3600  # Lock expires after 1 hour

Uniqueness Across Queues

sidekiq_options lock: :until_executed,
               unique_across_queues: true  # Same args on different queues = duplicate

ReliableFetch

v9 includes an optional reliable fetch strategy that provides crash recovery and lock-aware job acknowledgment:

Sidekiq.configure_server do |config|
  config[:fetch_class] = SidekiqUniqueJobs::Fetch::Reliable
end

Features:

  • Atomic LMOVE: Jobs move from queue to per-process working list atomically
  • Crash recovery: On startup, recovers jobs from dead worker processes
  • Lock-aware acknowledge: Confirms lock cleanup after job completion
  • Lock-preserving requeue: During shutdown, locks persist for requeued jobs

Web UI

Add to your routes:

require "sidekiq_unique_jobs/web"

This adds a Locks tab to the Sidekiq Web UI where you can browse, filter, and delete locks.

Testing

Disable uniqueness in your tests:

SidekiqUniqueJobs.config.enabled = false

Or use Sidekiq::Testing modes:

Sidekiq::Testing.inline! do
  # Jobs execute immediately, uniqueness still enforced
end

Upgrading from v8

v9 automatically migrates v8 lock data on first startup. No manual steps required.

Key changes:

  • Redis keys: 2 per lock (down from 13). Only digest:LOCKED hash and uniquejobs:digests sorted set.
  • Sidekiq 8+ only: Dropped Sidekiq 7 support.
  • Ruby 3.2+ only: Dropped older Ruby support.
  • Changelog removed: Use the reflection system for lock event observability.
  • Expiring locks unified: No separate expiring_digests sorted set. TTL-based locks use the same digests ZSET with expiry time as score.

Reflections

Observe lock lifecycle events without modifying behavior:

SidekiqUniqueJobs.reflect do |on|
  on.locked { |job| logger.info("Locked: #{job['class']}") }
  on.unlocked { |job| logger.info("Unlocked: #{job['class']}") }
  on.lock_failed { |job| logger.warn("Lock failed: #{job['class']}") }
  on.execution_failed { |job| logger.error("Execution failed: #{job['class']}") }
end

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-feature)
  3. Run tests (bundle exec rspec)
  4. Run linter (bundle exec rubocop)
  5. Commit and push
  6. Create a Pull Request

License

MIT