Use cases

# Serialize per resource

Run at most one job per resource at a time — never two balance syncs for the same account concurrently — while different accounts still run in parallel.

## The scenario

You sync a bank account's balance from an external provider. Running two syncs for the **same account** at the same time corrupts the balance — they race on the same rows. But two syncs for **different** accounts are completely independent and should run in parallel for throughput.

You don't care if a duplicate sync for account `42` gets *enqueued* while another is running — you only care that they never *execute* at the same time. That's exactly what `:while_executing` gives you: a lock taken on the server, keyed on the arguments, held only for the duration of `perform`.

## The worker

`:while_executing` locks just before `perform` runs and releases just after. Because the lock is keyed on the arguments, `account_id` becomes the thing that's serialized:

```ruby
class SyncAccountBalanceJob
  include Sidekiq::Job

  sidekiq_options lock: :while_executing,
                  on_conflict: :reschedule

  def perform(account_id)
    account = Account.find(account_id)
    account.sync_balance_from_provider!
  end
end
```

- `lock: :while_executing` — the lock is taken on the **server**, right before `perform`, and released right after. It has no effect at enqueue time.
- `on_conflict: :reschedule` — v9 locks are **non-blocking**: when a copy for the same account is already running, this copy's single acquisition attempt fails and the conflict strategy fires immediately. `:reschedule` re-enqueues this copy to run later (~5s by default) instead of dropping it. Because `:while_executing` conflicts happen during execution, this is a **server** conflict strategy.

## What happens on a duplicate

Two syncs for the same account, arriving close together.

Say two `SyncAccountBalanceJob` copies for account `42` land on your workers at nearly the same time:

1. **Copy A** reaches `perform`. `:while_executing` acquires the lock keyed on `[42]` and starts syncing.
2. **Copy B** reaches `perform` a moment later and makes its **single** attempt to acquire the same lock. It's held by A, so B's attempt fails right away — v9 locks are non-blocking, so B does **not** wait.
3. `on_conflict: :reschedule` fires immediately: B is re-enqueued to run later (~5s by default). It is not resumed in place when A finishes — it comes back around as a fresh job and tries again.
4. When B's rescheduled run lands and account `42` is free, it acquires the lock and syncs — the two runs happen one after another, never concurrently.

Meanwhile a `SyncAccountBalanceJob` for account `43` has a **different** digest, so it never contends with either — it runs in parallel.

## Why it serializes per resource

Uniqueness is computed from the worker class, the queue, and **the arguments**. Two calls with the same `account_id` produce the same digest and therefore compete for one lock; two calls with different `account_id`s produce different digests and never see each other. The `account_id` argument *is* the serialization key — no extra configuration needed.

If your `perform` takes arguments that shouldn't affect uniqueness (a timestamp, a request id), narrow the digest to just the resource id with `lock_args_method`:

```ruby
class SyncAccountBalanceJob
  include Sidekiq::Job

  sidekiq_options lock: :while_executing,
                  on_conflict: :reschedule,
                  lock_args_method: :unique_args

  def self.unique_args(args)
    [args.first] # serialize on account_id only, ignore the rest
  end

  def perform(account_id, requested_at)
    account = Account.find(account_id)
    account.sync_balance_from_provider!(as_of: requested_at)
  end
end
```

## The pitfall: it does not stop duplicate enqueues

`:while_executing` locks on the **server**, only around execution. It does **not** run at enqueue time, so it will not stop a duplicate copy from being *pushed onto the queue*. Fire `perform_async(42)` ten times and all ten land in the queue — `:while_executing` only guarantees they don't *run* at the same time; they run one after another.

> **Warning:** If duplicate copies piling up in the queue is a problem — not just concurrent execution — you want a client-side lock too. See [Enqueue once, run serialized](https://sidekiq-unique-jobs.zoolutions.llc/docs/enqueue-once-run-serialized).

## Where to go next

- **[Enqueue once, run serialized](https://sidekiq-unique-jobs.zoolutions.llc/docs/enqueue-once-run-serialized)** — when you need *both* a single queued copy *and* serialized execution, use `:until_and_while_executing`.
- **[Bounded concurrency](https://sidekiq-unique-jobs.zoolutions.llc/docs/bounded-concurrency)** — allow up to *N* concurrent runs of the same digest instead of exactly one, with `lock_limit`.