Concepts

Choosing a lock type

Start from what you want to prevent, and the right lock type falls out of a single decision table.

Start here#

Match your goal to a lock type, then read the section below it.

A lock type answers one question: when is the lock held? Every lock is acquired at either enqueue or the start of execution, and released at some later point. Find the row that matches what you're trying to prevent.

I want to…Lock typeUse case
prevent the same job sitting in the queue twice:until_executingDebounce duplicate enqueues
prevent duplicates until the job finishes:until_executedProcess exactly once
allow only one per time window:until_expired + lock_ttlOne per period
never run two copies at the same time:while_executingSerialize per resource
both — unique in the queue and serialized at run:until_and_while_executingEnqueue once, run serialized

Every option is set the same way — one lock: key in sidekiq_options. The sections below give the exact locks-at / unlocks-at behavior and the tradeoff for each.

:until_executing#

Locks at enqueue, unlocks just before perform starts.

Holds the lock from the moment the job is enqueued until just before perform begins. This debounces a burst of enqueues down to a single queued job.

Tradeoff: the lock releases the instant the job starts running, so a fresh copy can be enqueued while the original is executing. If you need the original to keep its exclusivity through execution, reach for :until_executed instead.

class SyncContactJob
  include Sidekiq::Job

  sidekiq_options lock: :until_executing

  def perform(contact_id)
    # Rapid re-triggers collapse into one queued job.
  end
end

See Debounce duplicate enqueues for a complete walkthrough.

:until_executed#

Locks at enqueue, unlocks after perform completes.

Holds the lock across the whole lifecycle — from enqueue until perform finishes. No duplicate can be enqueued or run while the original is still in flight. This is the lock most people mean when they say "unique job."

Tradeoff: the lock lives for the full push → done span, so a slow or stuck job keeps the digest locked the entire time. Pair it with lock_ttl as a safety valve if a job can hang.

class ChargeCustomerJob
  include Sidekiq::Job

  sidekiq_options lock: :until_executed

  def perform(customer_id)
    # Exactly one charge per customer_id from enqueue to completion.
  end
end

See Process exactly once for the full pattern.

:until_expired#

Locks at enqueue, unlocks only when the TTL expires.

Holds the lock from enqueue until its lock_ttl runs out — never on completion. This gives you time-boxed uniqueness: one job per window, regardless of how quickly it finishes. It's the right choice for "once per day" style jobs.

Tradeoff: because the lock releases by expiry rather than by code, the after_unlock callback never fires for this lock type. The TTL is measured from when the lock is created (at enqueue), not from when the job finishes — see TTL and timeouts.

class DailyReportJob
  include Sidekiq::Job

  sidekiq_options lock: :until_expired, lock_ttl: 86_400

  def perform(report_id)
    # Runs at most once per 24 hours per report_id.
  end
end

See One per period for choosing a window.

lock_ttl is required for :until_expired — without it the lock has no expiry and nothing releases it.

:while_executing#

Locks just before perform, unlocks after perform.

Holds the lock only for the duration of execution. Duplicates queue up freely, but no two copies run at the same time — each waits its turn (or follows the conflict strategy). Use it to serialize work against a shared resource.

Tradeoff: it does nothing about duplicate enqueues — the queue can hold many copies; they're merely run one at a time. The conflict happens on the server, so its on_conflict is a server-side strategy.

class RebuildIndexJob
  include Sidekiq::Job

  sidekiq_options lock: :while_executing

  def perform(index_name)
    # Only one rebuild of a given index runs at a time.
  end
end

See Serialize per resource for the details.

:until_and_while_executing#

Combines the queue lock and the execution lock.

The combination of the two behaviors above: locks at enqueue (unique in the queue) and locks again just before perform (serialized during execution). A duplicate can't sit in the queue, and even after the queue lock releases at the start of a run, a second copy can't execute concurrently.

Tradeoff: it's the strongest guarantee and the most machinery — two locks per job. Use it only when you genuinely need both properties; if you just need one, pick the simpler lock.

class DeliverInvoiceJob
  include Sidekiq::Job

  sidekiq_options lock: :until_and_while_executing

  def perform(invoice_id)
    # Unique in the queue, and never two deliveries running at once.
  end
end

See Enqueue once, run serialized for when this pairing earns its cost.

Aliases#

Some lock types have aliases that all resolve to the same implementation. Use whichever reads best in your code, but prefer the canonical name in the first column when in doubt.

CanonicalAlso accepted
:until_executed:until_completed, :until_performed, :until_processed, :until_successfully_completed
:until_executing:while_enqueued
:while_executing:around_perform, :while_busy, :while_working

There's also :while_executing_reject — a :while_executing lock that forces on_conflict: :reject on the server, sending conflicting jobs straight to the Dead set.

A common misunderstanding#

:while_executing does NOT prevent duplicate enqueues. It only stops two copies from running at the same time — the queue can still fill with duplicates, and each one runs in turn. If you also need the queue to stay unique, use :until_executed or :until_and_while_executing instead.