Overview
sidekiq-unique-jobs stops the same job from being enqueued twice or run concurrently, using Redis locks keyed on the job's arguments.
What it does#
Sidekiq will happily enqueue the same job a hundred times. Usually that's fine — but sometimes it isn't: a webhook that fires twice, a user who double-clicks "export", a cron tick that overlaps the previous run. sidekiq-unique-jobs makes a job unique: while a lock is held, a second copy with the same arguments is prevented (or logged, rejected, rescheduled — your choice).
A job opts in with one line:
class ChargeCustomerJob
include Sidekiq::Job
sidekiq_options lock: :until_executed
def perform(customer_id)
# Only one job per customer_id at a time.
end
endUniqueness is based on the worker class, the queue, and the arguments — so ChargeCustomerJob.perform_async(1) and ChargeCustomerJob.perform_async(2) are independent, but a second perform_async(1) while the first is still locked is a duplicate.
Two questions decide everything#
Which lock type, and what happens on a conflict.
Configuring a unique job comes down to two decisions:
- When should the lock be held? From enqueue until the job starts? Until it finishes? For a fixed time window? Only while it runs? That's the lock type — see Choosing a lock type.
- What should happen to a duplicate? Drop it, retry it later, replace the original, or send it to the dead set? That's the conflict strategy — see Conflict resolution.
Everything else — TTLs, timeouts, custom argument filtering, concurrency limits — is tuning on top of those two.
Two Redis keys per lock#
v9 is a ground-up rewrite. A lock is now just two Redis structures:
<digest>:LOCKED # Hash — the job IDs holding this lock, with metadata
uniquejobs:digests # ZSet — a global index of every active lockEvery lock operation runs inside a Lua script, so acquiring and releasing a lock is atomic even under heavy contention. If you're coming from v8, that's down from as many as thirteen keys per lock — see Upgrading to v9.
Where to go next#
- Installation — add the gem and wire up the middleware.
- Quick start — your first unique job in five minutes.
- Use cases — pick the page that matches what you're trying to prevent; each one is a complete, working worker.
- Choosing a lock type — a decision table from "I want to prevent X" to the lock that does it.