Use cases

Only the latest matters

When a newer job should overtake an older queued copy, use on_conflict: :replace so the latest payload wins.

The scenario#

A state snapshot where only the freshest version should run.

You sync a document's state to an external system. A user edits the document, you enqueue a sync. They edit again a second later, you enqueue another. The queue now holds two jobs for the same document — but the first one carries stale state. Running it wastes work and, worse, risks writing an older snapshot on top of a newer one.

What you want is simple: the newest enqueue should replace the one already waiting. The latest payload wins, the older copy never runs. That's exactly what on_conflict: :replace does.

The worker#

Lock the job for its whole push-to-done lifecycle with :until_executed, and set on_conflict: :replace so a duplicate enqueue swaps out the waiting job instead of being dropped. Uniqueness keys on the document_id argument, so each document is tracked independently.

class SyncDocumentStateJob
  include Sidekiq::Job

  sidekiq_options lock: :until_executed,
                  on_conflict: :replace

  def perform(document_id)
    document = Document.find(document_id)
    ExternalSync.push(document.snapshot)
  end
end

Now a burst of edits to the same document collapses to a single sync that carries the most recent state:

SyncDocumentStateJob.perform_async(42)   # enqueued, lock acquired
SyncDocumentStateJob.perform_async(42)   # replaces the first — newest wins
SyncDocumentStateJob.perform_async(99)   # different document, independent

What :replace does on a conflict#

When a second job arrives while the lock for that digest is held, :replace performs the swap in one motion:

  1. It deletes the job already sitting in the queue for that digest.
  2. It releases the lock that job was holding.
  3. It enqueues the new job and acquires the lock for it.

The end state is a single queued job carrying the latest arguments — never two, and never the stale one. Because the worker uses :until_executed, the lock is held from enqueue right through to the end of perform, so the replacement window covers the entire time a duplicate could otherwise pile up.

Uniqueness keys on worker class, queue, and args. Two edits to document 42 collide and replace; an edit to document 99 is a different digest and runs on its own.

Contrast with :log — which copy survives?#

The difference is entirely about which job is kept.

Both strategies leave you with exactly one job. They differ on which one — and that is the whole decision:

StrategyOn a duplicateWhich payload runs
:replaceDeletes the queued job and its lock, enqueues the new oneThe newest — latest args win
:logLogs the conflict and discards the incoming jobThe original — first args win

For a state snapshot, :replace is right: the freshest snapshot is the one worth sending. If instead you were debouncing an operation where the first request is authoritative and later ones are just noise, :log (the effective default) is the better fit — keep the original, drop the rest.

A note on what gets replaced#

:replace targets the queued job — the duplicate that is still waiting to run. It finds the existing job for the digest, removes it, and puts the new one in its place. It does not reach into a job that has already started executing; once perform is underway, that run is left alone and the lock behaves according to your lock type.

In practice this is what you want for a "latest wins" sync: you are racing enqueues, not interrupting work in flight. Pair :replace with :until_executed (as above) so the lock is held long enough for the replacement to matter.

For the full list of strategies and how to split client and server behavior, see Conflict resolution.