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 type | Use case |
| --- | --- | --- |
| prevent the same job sitting in the queue twice | `:until_executing` | [Debounce duplicate enqueues](https://sidekiq-unique-jobs.zoolutions.llc/docs/debounce-duplicate-enqueues) |
| prevent duplicates until the job finishes | `:until_executed` | [Process exactly once](https://sidekiq-unique-jobs.zoolutions.llc/docs/process-exactly-once) |
| allow only one per time window | `:until_expired` + `lock_ttl` | [One per period](https://sidekiq-unique-jobs.zoolutions.llc/docs/one-per-period) |
| never run two copies at the same time | `:while_executing` | [Serialize per resource](https://sidekiq-unique-jobs.zoolutions.llc/docs/serialize-per-resource) |
| both — unique in the queue **and** serialized at run | `:until_and_while_executing` | [Enqueue once, run serialized](https://sidekiq-unique-jobs.zoolutions.llc/docs/enqueue-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.

```ruby
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](https://sidekiq-unique-jobs.zoolutions.llc/docs/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.

```ruby
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](https://sidekiq-unique-jobs.zoolutions.llc/docs/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](https://sidekiq-unique-jobs.zoolutions.llc/docs/ttl-and-timeouts).

```ruby
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](https://sidekiq-unique-jobs.zoolutions.llc/docs/one-per-period) for choosing a window.

> **Note:** 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.

```ruby
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](https://sidekiq-unique-jobs.zoolutions.llc/docs/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.

```ruby
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](https://sidekiq-unique-jobs.zoolutions.llc/docs/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.

| Canonical | Also 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

> **Warning:** :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.