A failed payment can be replayed from the same payload. Two clicks can still dispatch two jobs for one order. You either want the second dispatch dropped, or you want it to wait until the first has finished. Those are different tools.
Queues in production · Lesson 2
Unique jobs and WithoutOverlapping
Video
01 Drop a duplicate, or make it wait
ShouldBeUnique drops a second dispatch while the first job is still queued or running. uniqueId() decides what “the same work” means.
class MarkOrderPaid implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public function uniqueId(): string
{
return (string) $this->orderId;
}
}WithoutOverlapping does not drop the second job. It holds a lock and releases the second job back onto the queue until the first one finishes. That is the right choice when both runs must happen, just not at the same time.
The snippet on stopping the same job running twice shows the lock, the pause, and the expiry.
Stop the same job running twice.
Use ShouldBeUnique when a second copy is useless. Use WithoutOverlapping when the second copy must run, only later.
02 Hold the second job until the first has finished
WithoutOverlapping is middleware. The key is the order id, the same value uniqueId() used. releaseAfter() is how soon the waiting job may be tried again. expireAfter() is when a lock left by a dead worker is dropped. It must be longer than the job timeout, or a killed worker blocks every later run until the lock happens to expire.
class Example
{
public function middleware(): array
{
return [
(new WithoutOverlapping((string) $this->orderId))
->releaseAfter(15)
->expireAfter(90),
];
}
}uniqueFor on a ShouldBeUnique job is a different clock: it is how long the “drop the duplicate” lock is held, and the lock also clears when the job finishes or exhausts its tries. Pick one tool. A job that is both unique and overlapping is two policies for one order, and the second dispatch can be dropped when you meant it to wait. The middleware is the one that matches “both clicks must be paid, just not together”.
Recap
You can drop a duplicate dispatch with ShouldBeUnique, or make it wait with WithoutOverlapping.
uniqueId()is what 'the same work' means.WithoutOverlappingdoes not drop the second job. It releases it back.expireAfter()must outlast the job, or a dead worker blocks the order.
Check yourself · 3 questions
Question 1 of 3Both clicks must record something, just not at the same time. Which tool?