On this page · 6 sections
TL;DR
Laravel 13.36.0 and 13.37.0 fix several ways a unique job or queued listener could keep its lock after the work never reached the queue. With the default uniqueFor of 0, such a lock never expires, so every later dispatch with the same unique ID was silently skipped. 13.37.0 also fixes a scheduler race that skipped tasks using both onOneServer() and withoutOverlapping() on every server, which one project saw 1 to 3% of the time.
Unique Lock Fixes in Laravel 13.36 and 13.37 #
| Release | Fix | When the lock got stuck or the job was skipped |
|---|---|---|
| 13.36.0 | Unique queued listener locks released on rollback (#61974) | A ShouldQueueAfterCommit unique listener dispatched inside a transaction that rolled back |
| 13.37.0 | Unique lock released when pushing a job fails (#62010) | The queue push threw, for example Redis unreachable or SQS throttling |
| 13.37.0 | Unique lock released when pushing a queued listener fails (#62045) | The same push failure for a unique queued listener |
| 13.37.0 | Unique queued listener locks released in Queue::fake() (#62025) | Tests that fake the queue more than once in one process |
| 13.37.0 | onOneServer() with withoutOverlapping() race fixed (#62007) | Several scheduler servers checking the same task at the same moment |
A Failed Push Left the Lock Held Forever #
PR #62010 describes the most common case. PendingDispatch acquires the unique lock in shouldDispatch() and then pushes the job, but nothing released the lock when the push threw. Once the queue was back, the job could not be dispatched again until the lock expired, and with the default uniqueFor of 0 it never does:
class SyncInventory implements ShouldQueue, ShouldBeUnique
{
//
}
SyncInventory::dispatch(); // RedisException: Connection refused
// later, with the queue back up
SyncInventory::dispatch(); // skipped, the lock is still heldFrom 13.37.0 the lock is released when dispatching throws, the exception is rethrown, and the unique job information added to the context is removed. The release goes through UniqueLock::release(), so it only releases the lock that dispatch owns. 13.37.0 applies the same fix to unique queued listeners (#62045).
Rolled-Back Transactions and Unique Queued Listeners #
In 13.36.0, PR #61974 fixes the same symptom for listeners that implement ShouldQueueAfterCommit and ShouldBeUnique. When the event was fired inside a transaction that rolled back, the listener was never queued but its lock stayed, so the next event for the same order was skipped. An earlier fix, #55420, already released the lock on rollback for unique jobs, but the check only looked for ShouldBeUnique, while queued listeners are pushed as a CallQueuedListener that reports uniqueness through shouldBeUnique(). The PR adds the check the queue handler already used.
The onOneServer and withoutOverlapping Race #
PR #62007, which closes issue #50330, explains why a task using both guards was occasionally skipped on every server: CacheEventMutex::exists() checked whether a task was running by acquiring the event lock and releasing it, so for a moment other servers saw the task as overlapping. The fix uses Lock::isLocked(), which reads the lock's state without acquiring it. The author notes that the failure is silent and was seen about 1 to 3% of the time on one project, so it may be under-reported. An earlier attempt, #54430, did not handle locks kept in a different cache store, such as the database cache_locks table or a Redis lock_connection.
Upgrading and Protecting Older Versions #
- Upgrade to Laravel 13.37.0 or later; it carries the 13.36.0 fix as well.
- Until then, give unique jobs and listeners a finite
uniqueFor, so a lock left behind by a failed push or a rollback expires instead of blocking the job forever. - Look for unique jobs that stopped running after a queue outage: their locks may still be held in the cache store and can be cleared there.
- If scheduled tasks use both
onOneServer()andwithoutOverlapping(), check run monitoring for skipped runs; after 13.37.0 the race no longer applies. - Test suites that call
Queue::fake()more than once in one process now see unique queued listeners pushed each time.
The Sentinel’s Call
Every Laravel 13 application that uses unique jobs or unique queued listeners should move to 13.37.0, because a single failed push or rolled-back transaction could block a job for good with the default uniqueFor of 0. Applications that cannot upgrade yet should set a finite uniqueFor. Schedulers running on several servers also gain a fix for tasks that were silently skipped.
Our judgement, drawn only from the facts and sources above.
Sources
- laravel/framework#62010: [13.x] Release the unique lock when pushing a job to the queue fails github.com · Oct 14, 2026
- laravel/framework#62007: [13.x] Fix race condition with jobs using both `onOneServer()` and `withoutOverlapping()` github.com · Oct 13, 2026
- laravel/framework#61974: [13.x] Fix unique queued listener locks not being released when the transaction is rolled back github.com · Oct 12, 2026
More on PHP and Laravel
2 other articles, newest first
Edited and approved by Jarosław Krzemiński. Spotted a mistake? Report a correction or read the corrections policy.




Comments