Prevents duplicate Sidekiq jobs
Prevents duplicate Sidekiq jobs. Uses Redis locks to ensure only one job with the same arguments runs at a time. Full documentation → — concepts, use-case guides, and reference.
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.
gem "sidekiq-unique-jobs", "~> 9.0"
# 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.
: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
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 }
…
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
sidekiq_options lock: :until_expired,
lock_ttl: 3600 # Lock expires after 1 hour
sidekiq_options lock: :until_executed,
unique_across_queues: true # Same args on different queues = duplicate
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:
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.
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
v9 automatically migrates v8 lock data on first startup. No manual steps required.
Key changes:
digest:LOCKED hash and uniquejobs:digests sorted set.expiring_digests sorted set. TTL-based locks use the same digests ZSET with expiry time as score.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
git checkout -b my-feature)bundle exec rspec)bundle exec rubocop)MIT
until_expired locks leak Redis memory (expiring_digests + INFO keys) on 8.1.0
rspec.yml's gemfile matrix axis is inert: no step sets BUNDLE_GEMFILE, so 8 cells test 4 things
Installation guide version mismatch
Unable to unlock the job.
Lock digests not always removed after forced sidekiq worker shutdown
uniquejobs:digests sorted set seems to grow forever
After sidekiq shutdown (with running job) lock digest is not pushed back
until_executed lock not releasing after job finishes
PRIMED key grows indefinitely when job re-queues itself in `after_unlock` callback
Catched a deadlock on Sidekiq 7+ set up using middleware.