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
endNow 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, independentWhat :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:
- It deletes the job already sitting in the queue for that digest.
- It releases the lock that job was holding.
- 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.
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:
| Strategy | On a duplicate | Which payload runs |
|---|---|---|
:replace | Deletes the queued job and its lock, enqueues the new one | The newest — latest args win |
:log | Logs the conflict and discards the incoming job | The 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.