Laravel ships a release most weeks, each with a few hundred changes, and the handful that decide how your background jobs behave are scattered among them. This page is every one of those, from 13.0.0 onwards — assembled from the framework's own history by what each commit touched, not by what its subject line happened to say.
What changed in Laravel 13 queues#
-
Queue routing.
Queue::route(ProcessPodcast::class, 'podcasts', 'redis')declares which connection and queue a job class defaults to, centrally, instead of at every dispatch site. -
Job settings became attributes. Thirteen of them by 13.42 —
#[Tries],#[Backoff],#[Timeout],#[FailOnTimeout],#[UniqueFor]and the rest — including#[DebounceFor], which is new behaviour rather than a restatement. -
The queue will tell you what is in it. The jobs themselves as collections, a single
totalSize()and, on theQueuecontract for the first time, the pending, delayed and reserved counts and the creation time of the oldest pending job. Those four are what breaks custom drivers. - Pausing grew up. A global switch and events for workers entering and leaving the paused state, plus worker output that no longer makes a paused queue look like an empty one.
-
Twelve more queue events, nineteen at the end of 12.x against thirty-one now. Most
usefully
UniqueJobSkipped, which makes a droppedShouldBeUniquedispatch visible for the first time.
What breaks on the way from Laravel 12#
The queue section of the upgrade guide is marked low impact, and for most applications it is. It is not low impact if you maintain a custom queue driver, or if you listen to queue events — which, if you alert on backlog, you do.
| Change | In | What to do |
|---|---|---|
| JobAttempted carries the exception, not a boolean | 13.0.0 | Read the note |
| The Queue contract gained four size methods | 13.0.0 | Read the note |
| QueueBusy::$connection renamed to $connectionName | 13.0.0 | Read the note |
The changes that change how you run queues#
Most of the 256 entries below are one line because one line is all they need. These are the ones that are worth reading properly.
Pausing a queue became a real operation#
More of this cycle went into pausing than into anything else.
Laravel 12.40 introduced per-queue pausing and 12.40.2 a version with
a TTL; 13.x turned it into something you would reach for during an
incident. There is a global switch now —
queue:pause --all, Queue::pauseAll() —
events for workers entering and leaving the paused state, and worker
output that says which queues are paused instead of looking identical
to an empty queue.
#61126, #61142, #59895, #61139
The framework will now tell you what is in a queue#
Laravel 12.19 gave every first-party driver pending, delayed and
reserved counts and the creation time of the oldest pending job — the
one number that can distinguish a busy queue from a stuck one.
Laravel 13 added the rest on top: the jobs themselves as collections,
the same across a whole connection, and a single
totalSize(). SQS and Beanstalkd cannot list their
contents, so those return empty, zero or null there.
This is the change with the widest blast radius in the major,
because the four methods from 12.19 are now on the
Queue contract. Every custom driver has to implement
them.
#59511, #59997, #61231, #61373, #58914
Job configuration moved to attributes#
Thirteen attributes now cover what used to be properties and
methods — #[Tries], #[Backoff],
#[Timeout], #[FailOnTimeout],
#[UniqueFor] and the rest — plus
#[DebounceFor], which is genuinely new behaviour rather
than a restatement: a job that collapses a burst of dispatches into
one run after the burst ends.
A worker that says what it is doing#
Illuminate\Queue\Events held nineteen classes at the
end of the 12.x line and holds thirty-one in 13.42. The ones that change what you can observe:
WorkerIdle for a poll that found nothing,
JobReleased and JobInterrupted for the two
ways a job ends without finishing, a reason and a job count on
WorkerStopping, and
UniqueJobSkipped — which finally makes a dropped unique
dispatch visible instead of silent. JobProcessed and
JobAttempted report how long the attempt took, failed
attempts included, and Queue::whenJobRunsLongerThan()
calls you back when that is over a threshold. Jobs can also see the signals
sent to their worker and wind themselves down rather than being
killed mid-flight.
#60134, #61108, #61412, #58341, #59833, #61039, #61672, #62118, #62141
Routing and forwarding: one place to say where jobs go#
Queue::route() declares which queue a job class belongs
on in one place rather than at every dispatch site, and
Queue::forward() redirects a queue's new dispatches
elsewhere. Both exist for the same reason: moving work between queues
used to mean editing every place that named the queue. Both are still
registered in code, so it now takes one line and a deploy rather than
a search and replace.
What this page counts as a queue change#
An entry is here if the commit behind it touched a file that decides how background work behaves:
Illuminate\Queue, Illuminate\Bus, the queue and bus contracts, the
queue:* commands, the queue fakes — plus the queue-aware files elsewhere, such as the queued
mailable that was being pushed to the wrong queue when its name was a backed enum. Matching on paths rather
than on wording is what catches a change whose subject line never says "queue".
Deliberately outside it: the scheduler, which is not the queue however much
withoutOverlapping() smells like one; broadcasting, where BroadcastEvent genuinely is a
queued job but including it would drag in every websocket change for the sake of a handful; and the cache, which
backs the unique and overlapping locks without being part of the queue.
Rebuilt from laravel/framework's git history, release by release. Every entry links to the pull request, or to the commit for the handful of changes pushed straight to the branch. The notes are written by hand; the list is not.
Every change, by release
| Version | Released | Changes | Most notable |
|---|---|---|---|
| 13.41.0 | 10 Nov 2026 | 2 | An after-response job that throws is reported once and no longer stops the jobs after it |
| 13.40.0 | 3 Nov 2026 | 3 | A batched job that fails on a deleted model is counted by its batch |
| 13.39.0 | 27 Oct 2026 | 8 | Sync jobs report a duration too |
| 13.38.0 | 20 Oct 2026 | 5 | Queued listeners, mailables and notifications honour #[CountCrashesAsExceptions] |
| 13.37.0 | 16 Oct 2026 | 5 | The same fix for unique queued listeners |
| 13.36.0 | 13 Oct 2026 | 9 | A rolled-back transaction releases a unique queued listener's lock |
| 13.35.0 | 6 Oct 2026 | 4 | Bulk SQS dispatch puts the queue name in JobQueueing and JobQueued, not the enum |
| 13.34.0 | 29 Sep 2026 | 7 | #[CountCrashesAsExceptions]: a job that keeps killing its worker stops being retried |
| 13.33.0 | 22 Sep 2026 | 6 | queue:retry-batch skips bad batch IDs and exits non-zero |
| 13.32.0 | 15 Sep 2026 | 5 | Enums accepted by the pause and resume methods |
| 13.31.0 | 8 Sep 2026 | 6 | totalSize(): everything queued on a connection, in one number |
| 13.30.1 | 1 Sep 2026 | 1 | connection and queue on WorkerStopping |
| 13.30.0 | 1 Sep 2026 | 4 | Add totalXSize methods to Cloud Queue |
| 13.27.0 | 25 Aug 2026 | 4 | The after-commit rollback path stopped releasing locks it never took |
| 13.26.0 | 18 Aug 2026 | 10 | Queue::forward(): send a queue's new dispatches somewhere else |
| 13.25.0 | 11 Aug 2026 | 8 | A global pause switch: queue:pause --all |
| 13.24.0 | 4 Aug 2026 | 13 | Unique locks became owner-checked |
| 13.23.0 | 27 Jul 2026 | 1 | Reverted the 13.22 ShouldBeUniqueUntilProcessing lock change |
| 13.22.0 | 24 Jul 2026 | 5 | add the exception to JobReleasedAfterException |
| 13.21.0 | 21 Jul 2026 | 5 | InvalidPayloadException names the job and the queue |
| 13.20.0 | 14 Jul 2026 | 6 | Fix SQS queue size null coalescing precedence |
| 13.19.0 | 7 Jul 2026 | 6 | Bulk SQS dispatch uses SendMessageBatch |
| 13.18.1 | 2 Jul 2026 | 2 | Support inspecting delayed jobs on the queue fake |
| 13.18.0 | 30 Jun 2026 | 4 | WorkerStopping exposes the jobs-processed count and last job timestamp |
| 13.17.0 | 23 Jun 2026 | 2 | Should Not Retry Exception Handler |
| 13.16.0 | 16 Jun 2026 | 4 | Add missing @throws \JsonException annotations |
| 13.15.0 | 9 Jun 2026 | 14 | Allow enums in Queue::route |
| 13.14.0 | 4 Jun 2026 | 2 | Add queue to InspectedJob |
| 13.13.0 | 2 Jun 2026 | 5 | Bus::bulk(): push many jobs at once, grouped by connection and queue |
| 13.12.0 | 26 May 2026 | 6 | Update Worker.php |
| 13.11.0 | 19 May 2026 | 1 | Dedicated Cloud Queue |
| 13.10.0 | 19 May 2026 | 10 | WorkerIdle: the event for a worker with nothing to do |
| 13.9.0 | 13 May 2026 | 13 | SQS payloads over 1 MB can overflow to a cache store |
| 13.8.0 | 5 May 2026 | 6 | The same across every queue: allPendingJobs() and friends |
| 13.7.0 | 28 Apr 2026 | 9 | Jobs can react to the signals sent to their worker |
| 13.6.0 | 21 Apr 2026 | 7 | Debounceable jobs: #[DebounceFor] |
| 13.5.0 | 14 Apr 2026 | 6 | Support #[Delay] attribute on queued mailables |
| 13.4.0 | 7 Apr 2026 | 5 | Reading the jobs inside a queue, from the framework |
| 13.3.0 | 1 Apr 2026 | 9 | Add BatchStarted event |
| 13.2.0 | 24 Mar 2026 | 5 | Allow opting out of worker Job exception reporting |
| 13.1.1 | 18 Mar 2026 | 1 | Break queue dependency |
| 13.1.0 | 18 Mar 2026 | 2 | Use Carbon::now() and Date::now() instead of now() helper |
| 13.0.0 | 17 Mar 2026 | 20 | JobAttempted carries the exception, not a boolean |
8 releases in this range changed nothing about the queue and so appear nowhere below: 13.42.0, 13.29.0, 13.28.0, 13.26.1, 13.21.1, 13.16.1, 13.11.2, 13.11.1. Named here so that a version missing from the table reads as “nothing to report” rather than as a gap in the scan.
Laravel 13.41.0 10 Nov 2026 · 2 changes
-
Fix An after-response job that throws is reported once and no longer stops the jobs after it #62313 #
Upstream: “Fix `dispatch()->afterResponse()` trying to render exception”
When a job sent with
dispatch(…)->afterResponse()ordispatchAfterResponse()threw, the exception escaped the end of the request. Laravel reported it, then tried to render an error page for a response that had already gone out, which logged a second error:Cannot modify header information - headers already sent. Worse, it stopped the shutdown, so the after-response jobs queued behind the failing one never ran, and neither did anything else registered withapp()->terminating()after it. An Artisan command that dispatched after the response behaved the same way, with the exception printed under the command's output.Each after-response job is now wrapped in
rescue(). The exception is reported once, through your exception handler, and the next job runs. Two failures that used to be loud are now quiet. WithBus::withoutDispatchingAfterResponses(), which runs these jobs at dispatch (see #62130), the exception used to reach the code that dispatched the job. It is now reported and that code carries on. And a feature test whose request dispatches a failing after-response job used to error. It now passes with the failure only in the log, so useExceptions::fake()andExceptions::assertReported()if the test should notice. -
Fix BeanstalkdJob docblocks: the job ID is a string (no behaviour change) #62306 #
Upstream: “Fix BeanstalkdJob job ID and job property types”
Laravel 13.40.0 3 Nov 2026 · 3 changes
-
Fix A batched job that fails on a deleted model is counted by its batch #62280 #
Upstream: “Record failed batch jobs when the model is missing”
When a batched job's model was deleted before the job ran, and the job had no
#[DeleteWhenMissingModels], the job went tofailed_jobsbut the batch never heard about it. The failure handler unserialized the job to find its batch, hit the sameModelNotFoundExceptionand gave up. The batch kept a failed count of zero, so it was never cancelled, itscatchandfinallycallbacks never ran, andqueue:retry-batchcould not retry the job, because its ID was not in the batch's failed job IDs.The handler now takes the batch ID from the payload and records the failure, so the batch treats it like any other failed job. It cancels unless it allows failures,
catchruns, andfinallyruns once every job has run. The job's ownfailed()method still does not run, because there is no job object to call it on. -
Fix queue:work prints RELEASED for a job that was put back #62180 #
Upstream: “Show released jobs as released in queue:work output”
A job that called
$this->release(), or was put back by middleware such asRateLimitedorWithoutOverlapping, returned normally, soqueue:workprinted a greenDONEfor an attempt that was going to run again. It now prints a yellowRELEASED. A release after an exception still prints a yellowFAIL, and the--jsonoutput already said"result":"released", so neither of those changes. Anything that countsDONElines in worker logs will see fewer of them, because the releases are no longer among them. -
Feature Queue::whenJobRunsLongerThan(): a callback for slow attempts #62141 #
Upstream: “13.x-whenJobRunsLongerThan”
Queue::whenJobRunsLongerThan(30000, fn ($job, $ms) => …)in a service provider'sboot()calls the closure after any attempt that took longer than the threshold, with the queue job and its runtime in milliseconds. A number is read as milliseconds, the same as inDB::whenQueryingForLongerThan(), and aCarbonIntervalworks too. It is aJobAttemptedlistener reading #62118's duration, so it covers the worker and the sync queue, and an attempt that threw counts as well as one that returned.$jobis the queue job rather than your job class, so$job->resolveName()gives you the name.It runs once the attempt is over. A job that is still running has not reached it yet, and a job killed by
--timeoutnever will: the worker dies beforeJobAttemptedfires, so the slowest attempts are the ones it misses. The database version fires once when a request's total query time crosses the line. This one checks each attempt on its own and fires for every attempt over it.
Laravel 13.39.0 27 Oct 2026 · 8 changes
-
Fix queue:retry all no longer throws a TypeError on non-scalar failed job IDs, such as MongoDB's ObjectId #62208 #
Upstream: “Cast failed job ids to strings”
-
Fix illuminate/bus loads without illuminate/queue installed (no change inside a Laravel app) #62182 #
Upstream: “Use ReadsClassAttributes in Bus”
-
Feature Sync jobs report a duration too #62155 #
Upstream: “13.x ensure sync queue passes duration too”
On the
syncconnectionJobProcessed::$durationwas alwaysnull.SyncQueuenow timesfire()the way the worker does and passes the result toJobProcessedandJobAttempted, so a duration listener gets a number from a queued job run withdispatchSync(). Thedeferredandbackgroundconnections run through the same code and get it as well.If you subclass
SyncQueue,raiseAfterJobEvent()andraiseJobAttemptedEvent()each gained a$durationparameter. An override with the old signature is now a fatal error when the class loads, so add the parameter. -
Fix The database queue no longer fails a job over a deadlock while reserving it #62145 #
Upstream: “Don't fail database queue jobs on concurrency errors while reserving”
Since #58978 in 12.54, any exception thrown while reserving a job on the database driver has failed that job. The change was aimed at jobs that can never be reserved, such as an attempts counter overflowing its column. It also caught deadlocks, lock wait timeouts and SQLite's
database is locked, so contention between workers alone could put a job infailed_jobswithouthandle()having run.A concurrency error now rolls back and leaves the job where it was. The worker reports the exception, sleeps a second and picks the job up on a later poll. Any other exception during reservation still fails the job, and 12.x keeps the old behaviour as of 12.70.2. To find the rows this already caught, search the
exceptioncolumn offailed_jobsforDatabaseQueue->pop(. That attempt never reachedhandle(). -
Feature Bus::withoutDispatchingAfterResponses() works on a faked bus #62130 #
Upstream: “Add withoutDispatchingAfterResponses to BusFake”
A job sent with
dispatchAfterResponse()from the test body, or from code the test calls directly, never runs, because nothing ends a request. (One dispatched inside$this->get()does run when the call returns, since the test client terminates the request.)Bus::withoutDispatchingAfterResponses()exists to run such jobs straight away. On a faked bus that call threwCall to undefined method, so a test could not fake some jobs and still run the after-response one. AfterBus::fake([SendNewsletter::class])it now works: the listed jobs stay faked and the others run at dispatch. -
Fix More docblock types corrected across the framework, the queue classes among them (no behaviour change) #62125 #
Upstream: “Fix incorrect docblocks”
-
Fix queue:retry no longer stops at a job whose model was deleted #62124 #
Upstream: “Retry remaining jobs when a job's model no longer exists”
queue:retryunserialises each failed job to readretryUntil()and its queueable options, and unserialising aSerializesModelsjob reloads its models. One deleted row threwModelNotFoundExceptionand ended the command. Every job after it stayed infailed_jobs, and running the command again stopped at the same job until someone found it and ranqueue:forget.$deleteWhenMissingModelsdid not help, because only the worker reads it.The payload now goes back on the queue untouched and the command carries on. From there the worker deletes the job if it sets
$deleteWhenMissingModelsand otherwise fails it again, so expect that row back infailed_jobswith theModelNotFoundExceptionas its exception. Any other exception thrown while unserialising still stops the command. -
Feature JobAttempted carries a duration, so a failed attempt has a runtime #62118 #
Upstream: “add duration to JobAttempted event”
JobProcessedonly fires whenfire()returns, which kept #61672's duration away from the attempts you most wanted timed. The worker now stops the clock in afinallyand passes the same milliseconds toJobAttempted::$duration, so one listener can time every attempt that ran, whether it succeeded or threw.JobFailedandJobExceptionOccurredstill carry no duration.It is
nullwhen the job never ran, as when the worker fails it for having used up its attempts beforefire(). A timed-out attempt has no duration either: the alarm handler kills the worker first, soJobAttemptednever fires. WithWorker::$killOnTimeout = falsethe worker survives and the event arrives with the time spent up to the timeout.
Laravel 13.38.0 20 Oct 2026 · 5 changes
-
Fix Docblock types corrected across the framework, QueuedClosure among them (no behaviour change) #62100 #
Upstream: “Fix incorrect docblocks”
-
Fix Batch and Queueable docblock types corrected (no behaviour change) #62097 #
Upstream: “Fix incorrect docblocks”
-
Fix PendingBatch generic type corrected (no behaviour change) #62096 #
Upstream: “Fix PendingBatch TParam type”
-
Change ThrottlesExceptionsWithRedis drops an unused import (no behaviour change) #62087 #
Upstream: “Remove unused imports”
-
Feature Queued listeners, mailables and notifications honour #[CountCrashesAsExceptions] #62048 #
Upstream: “feat(queue): support CountCrashesAsExceptions on queued listeners, mailables and notifications”
The same gap as #61899, for the crash counter from #61737. The worker reads the flag off the wrapper —
CallQueuedListener,SendQueuedMailable,SendQueuedNotifications— and none of them copied it, so on a listener, mailable or notification the attribute was ignored without a warning. A listener that ran the worker out of memory kept coming back untiltriesran out, which with$tries = 0was never.Now copied like
failOnTimeout, as the attribute or the$countCrashesAsExceptionsproperty. It still needsmaxExceptionsset to do anything. Jobs already queued before the upgrade were serialised without the flag, so they keep the old behaviour until they finish.
Laravel 13.37.0 16 Oct 2026 · 5 changes
-
Fix The same fix for unique queued listeners #62045 #
Upstream: “Release the unique lock when pushing a queued listener fails”
The listener path takes its lock in the event dispatcher, not in
dispatch(), so it needed the fix separately. A unique queued listener whose push throws, or whoseviaConnection()orviaQueue()throws, now releases its lock before the exception leavesEvent::dispatch(). -
Fix Queue::fakeFor() releases unique queued listeners' locks too #62025 #
Upstream: “Release unique queued listener locks in Queue::fake()”
-
Fix A dispatch that fails to reach the queue releases its unique lock #62010 #
Upstream: “Release the unique lock when pushing a job to the queue fails”
dispatch()takes aShouldBeUniquejob's lock before it pushes. If the push threw — Redis unreachable, SQS throttling, a payload too large to send — the exception reached you and the lock stayed. Retrying the dispatch then did nothing, because the lock said a copy was already queued, and nothing was. The lock lasted untiluniqueForran out, or for good without one. A failed push now releases the lock before it rethrows, so the retry goes through. -
Feature queue:failed --queue: list one queue's failed jobs #61971 #
Upstream: “Add `--queue` option in queue:failed”
php artisan queue:failed --queue=emailslists only the failed jobs that came from that queue, and works with--json. The filter runs after the provider has returned every failed job, so on a largefailed_jobstable it is no faster than the unfiltered command. -
Feature queue:forget takes several IDs, deletes the ones it finds, and exits 1 if any were missing #61901 #
Upstream: “Allow queue:forget to accept multiple IDs”
Laravel 13.36.0 13 Oct 2026 · 9 changes
-
Fix A rolled-back transaction releases a unique queued listener's lock #61974 #
Upstream: “Fix unique queued listener locks not being released when the transaction is rolled back”
The rollback callback that frees a
ShouldBeUniquejob's lock checked whether the job implementedShouldBeUnique. A queued listener is wrapped in aCallQueuedListener, which does not, so a unique listener queued after commit took its lock at dispatch and kept it when the transaction rolled back. Every later event with the same unique ID was dropped, silently — the listener path returns without firingUniqueJobSkipped— until the lock expired. With nouniqueFor, the lock never expires.If a unique listener has stopped running for one particular ID and nothing failed, look for a stale
laravel_unique_job:key in the lock store. Upgrading stops new ones; it does not clear the ones already there. -
Performance Redis bulk push writes its notify entries in chunks of 100, not one call per job #61965 #
Upstream: “Batch Redis queue notification pushes”
-
Change Removed the duplicate kill callback call the 12.x merge-up left behind (never in a release) #61940 #
Upstream: “Update Worker.php”
-
Fix Failover and round-robin mailers honour their retry_after (the transport's, not the queue's) #61927 #
Upstream: “Fix mail failover and roundrobin transports ignoring retry_after”
-
Feature Worker::$flushStickyConnections: stop one write pinning a worker to the primary #61915 #
Upstream: “Allow queue workers to flush sticky connections between jobs”
A read/write connection with
'sticky' => truesends reads to the write connection once anything has been written, for the rest of the request, so you read back what you just wrote whatever the replica lag. A worker is one request that lasts for hours. After the first job that writes, every read in every later job goes to the primary, and the replicas do nothing for that worker until it restarts.Set
Worker::$flushStickyConnections = truein a service provider'sboot()and the worker forgets the write state before each job. It is off by default. With it on, a job's reads start on the replica, so a job that reads a row the dispatching request has just written can hit replica lag, the way the first job a fresh worker runs always could. -
Feature Queued mailables honour #[DeleteWhenMissingModels] and #[FailOnTimeout] #61899 #
Upstream: “Support `DeleteWhenMissingModels` and `FailOnTimeout` on queued mailables”
SendQueuedMailablecopiedtries,timeoutand the rest off the mailable, but not these two, so both were ignored on a mailable. A mailable whose model was deleted before it sent failed outright withModelNotFoundExceptionand landed infailed_jobs. A timed-out one was retried until it ran out of tries. Both now work, as the attribute or as the property. -
Feature queue:work --json: paused and resumed lines name the connection #61896 #
Upstream: “add connection the the status json output”
-
Change queue:work --json: the stopped line names the connection and queue #61858 #
Upstream: “Include queue and connection in queue:work --json stop output”
-
Change Worker::killUsing() backported to 12.x (13.x already had it) #61623 also 12.x #
Upstream: “Make the worker timed out exit code reachable”
Laravel 13.35.0 6 Oct 2026 · 4 changes
-
Fix Bulk SQS dispatch puts the queue name in JobQueueing and JobQueued, not the enum #61828 #
Upstream: “13.x normalize enum in bulk”
The jobs went to the right queue, but a
bulk()push to SQS with a backed enum queue name passed the enum case itself toJobQueueingandJobQueued, where every other path passes the string. A listener that logs or counts by$event->queuegot an object it could not concatenate. Now it gets the queue name. -
Fix Queue::fakeFor() releases the unique locks it took #61824 #
Upstream: “Release unique job locks in `Queue::fakeFor()`”
A
ShouldBeUniquejob pushed insideQueue::fakeFor()orfakeExceptFor()took its lock in the real cache, and the lock outlived the fake. The same job dispatched later in the test, against the real queue, was skipped as a duplicate. The locks are now released when the callback returns. -
Feature Queue::assertPushed() and Bus::assertDispatched() accept an array of properties #61770 #
Upstream: “Allow fake assertions to accept an array of properties”
Queue::assertPushed(SendInvoice::class, ['invoice' => $invoice])replaces the closure that compared one property. Every key has to match a property on the job: models are compared withis(), and everything else with===, so'id' => '5'does not match an integer 5.assertPushedOn(),assertNotPushed()and the Bus, Event and Notification fakes take the array too. -
Feature queue:work --memory takes a percentage of memory_limit #61753 #
Upstream: “Support percentages for queue:work --memory”
--memory=80%stops the worker once it holds 80% of PHP'smemory_limit, so the threshold follows the container's PHP config instead of being a megabyte figure copied into every deploy script. As with the megabyte form, the check runs between jobs: it restarts a worker that has grown, and does nothing for the one job that blows straight past the limit.With
memory_limit=-1a percentage turns the check off. The limit works out negative, and the worker treats a limit of zero or less as no limit, without a warning. Plenty of CLI setups run with-1, so check whatphp -isays on the worker hosts before switching.
Laravel 13.34.0 29 Sep 2026 · 7 changes
-
Feature #[CountCrashesAsExceptions]: a job that keeps killing its worker stops being retried #61737 #
Upstream: “Allow jobs to count worker crashes towards maxExceptions”
#[MaxExceptions]only counted exceptions the worker caught. A job that ran the process out of memory or segfaulted it threw nothing: the worker died, the reservation expired, and the job came back for another try, and took the next worker down too. Onlytriesstopped it, so a job with a hightriesfor flaky APIs could kill workers over and over.Put
#[CountCrashesAsExceptions]on the job and the worker writes ajob-processing:{uuid}marker to the cache before running it and deletes it afterwards. If the marker is still there at the next attempt, the previous one never finished, and that counts as one exception. Once the count reachesmaxExceptionsthe job is failed before it runs again. It does nothing withoutmaxExceptionsset, and a timeout kill clears the marker, so timeouts still count against tries rather than as crashes. -
Fix Queue::isPaused() accepts enums too, like pause and resume already did #61725 #
Upstream: “13.x queue isPaused allow enums”
-
Change queue:restart exits 1 when restarting is disabled #61706 #
Upstream: “13.x allow restart command to error if restarting is disabled”
With
Worker::$restartableset tofalse, workers ignore the restart signal.queue:restartused to write it anyway and print that it had been broadcast. Now it prints an error and exits 1, so a deploy script that runs it can fail where it used to pass. -
Change Worker docblocks: the last-restart timestamp can be null (no behaviour change) #61683 #
Upstream: “Update Worker.php”
-
Feature JobProcessed carries how long the job took #61672 #
Upstream: “13.x duration in JobProcessed event”
JobProcessed::$durationis the time spent in the job'sfire()— its middleware andhandle()— in milliseconds, to two decimal places, timed with the worker'shrtimeclock. Recording job runtime used to take aJobProcessinglistener to stash a start time and aJobProcessedlistener to subtract it, keyed on the job ID; now it is one listener.In this release only the worker set it, and only on
JobProcessed, so a job that threw had no runtime anywhere. 13.39 closed both gaps: #62118 puts it onJobAttemptedand #62155 makes the sync queue set it. -
Change A timed-out job is told before it is killed #61651 #
Upstream: “Update Worker.php”
A job implementing
Interruptiblehas heard aboutSIGTERM,SIGINTandSIGQUITsince 13.7, but not about its own timeout. The timeout handler now callsinterrupted(SIGALRM)and firesJobInterruptedfirst, before it fails or releases the job and kills the worker. That call is the job's last chance to release a lock or write a checkpoint; anything it throws is reported and does not stop the kill.If you listen for
JobInterruptedand assumed it meant "a deploy is stopping this worker", it now also fires on every timeout. Check$signal. -
Feature queue:flush --queue: clear one queue's failed jobs #61630 #
Upstream: “Add --queue option to queue:flush”
php artisan queue:flush --queue=emailsdeletes only the failed jobs that came from that queue, and combines with--hours. The database, UUID database and file providers support it. DynamoDB still refuses to flush at all.Be careful with a custom failed-job provider.
FailedJobProviderInterface::flush()still declares only$hours, so a provider written against it drops the extra argument without complaint and flushes every queue. The command still reports that only the named queue was cleared.
Laravel 13.33.0 22 Sep 2026 · 6 changes
-
Fix queue:retry-batch skips bad batch IDs and exits non-zero #61653 #
Upstream: “Skip batches that cannot be retried in queue:retry-batch”
An ID for a batch that did not exist printed an error and then carried on into the batch's failed jobs anyway, which crashed the command on a null batch and abandoned any IDs after it. A batch with no failed jobs printed an error and then did nothing. Either way the command exited 0. Now it skips the bad ID, gets on with the rest, and exits 1 if any of them were skipped. A deploy script that runs
queue:retry-batchfor whatever IDs it has can now fail where it used to pass. -
Fix BatchFinished and BatchCanceled carry the batch as stored, not a stale copy #61644 #
Upstream: “Pass fresh batch to `BatchFinished` and `BatchCanceled` events”
Both events were dispatched with the in-memory
Batchthe last job was holding, loaded before the repository marked it finished or cancelled. ABatchFinishedlistener got a batch whosefinished()was false and whosefinishedAtwas null, and aBatchCanceledlistener got one whosecancelled()was false. The batch is now reloaded before dispatch. If a listener was re-fetching the batch to work around this, it can stop. -
Feature Worker::killUsing(): the timed-out exit code finally reaches the supervisor #61622 #
Upstream: “Make the worker timed out exit code reachable”
Worker::$timedOutExitCodehas been settable for years and has never done anything.kill()sends the process an uncatchableSIGKILLjust before itsexit($status), so the status never leaves: whatever is watching the worker sees a signal death, the same one it sees for an out-of-memory kill.Worker::killUsing(fn (int $status) => …)registers a callback that runs before theSIGKILLand can end the process some other way. Laravel Cloud uses it topcntl_exec()intosh -c 'exit 124'— same PID, no shutdown functions or destructors, like the kill, but the parent gets a real exit code. If the callback returns, theSIGKILLhappens as before. If you run workers under supervisord or Kubernetes, this is what lets a restart log tell a timeout apart from memory exhaustion. -
Feature Worker::$killOnTimeout: a timed-out job no longer has to take the worker with it #61591 #
Upstream: “Ability to opt out of killing the worker on job timeout”
A job that ran past its timeout has always cost you the whole worker: the
SIGALRMhandler fails or releases the job, firesJobTimedOut, thenSIGKILLs its own process and leaves the supervisor to start a new one. SetWorker::$killOnTimeout = falsein a service provider and the handler throws theTimeoutExceededExceptioninto the running job instead. The job takes the normal exception path — released or failed according to its tries — and the same process moves on to the next one.The kill stays the default for a reason. The exception lands wherever the job happened to be, so a job that catches
Throwableswallows its own timeout, and whatever it left half-done — an open transaction, a held lock — is now inherited by the next job instead of dying with the process. -
Change Timeout handler drops a redundant pcntl_signal() argument (no behaviour change) #61590 #
Upstream: “Update Worker.php”
-
Change Laravel Cloud: queue:retry pages through failed jobs, with backoff #61584 #
Upstream: “Improve Cloud job retries”
Laravel 13.32.0 15 Sep 2026 · 5 changes
-
Feature Enums accepted by the pause and resume methods #61464 #
Upstream: “Allow enums in queue pause/resume methods”
-
Change assign message group id when using fifo 5c6b069aab23 #
Laravel 13.31.0 8 Sep 2026 · 6 changes
-
Fix Fix RateLimited job middleware hitting limits that did not block the job #61449 #
-
Feature JobInterrupted #61412 #
Upstream: “`JobInterrupted` event”
-
Change Include connection and queue on WorkerStopping when worker is killed #61408 #
-
Feature totalSize(): everything queued on a connection, in one number #61373 #
Upstream: “Add totalSize method to Queue”
Pending, delayed and reserved added up across every queue, without keeping a list of queue names yourself. On Redis it finds the queues with
KEYS queues:*, orSCANon a phpredis cluster, so keep it off hot paths. SQS and Beanstalkd always return 0, whatever is queued, so an alert built on it stays quiet there. -
Fix The Redis driver is cluster-safe #61198 #
Upstream: “Make the Redis queue driver cluster-safe (`bulk()` node-less `MULTI`; `allQueueNames()` uses `KEYS`)”
Two problems on Redis Cluster with phpredis.
bulk()wrapped its pushes in aMULTIthat could span nodes, which a cluster refuses — so batch dispatch failed outright on some cluster setups. AndallQueueNames()ranKEYS, which now gives way toSCANon that connection.Everywhere else it still runs
KEYS, which is worth knowing about: it is O(n) over the keyspace and blocks the server for the duration. It runs on the inspection path rather than the pop path, so it will not slow your workers, but a dashboard polling it against a large keyspace is a different matter. -
Change reorder args ce2511a6ab5e #
Laravel 13.30.1 1 Sep 2026 · 1 change
Laravel 13.30.0 1 Sep 2026 · 4 changes
Laravel 13.27.0 25 Aug 2026 · 4 changes
-
Change Share cached AWS credentials across processes for SQS queue connections #61321 #
-
Fix The debounce max-wait window restarts after a run #61281 #
Upstream: “Start a new max wait window after a debounced job runs”
-
Fix The after-commit rollback path stopped releasing locks it never took #61234 #
Upstream: “Don't force release a unique lock the job never acquired”
The narrow follow-up to the owner check in 13.24. A
ShouldBeUniquejob dispatched inside a transaction registers a rollback callback that releases its lock if the transaction never commits — and that callback was still releasing unconditionally, so a dispatch that had found the lock already held would, on rollback, free somebody else's. It now returns early when no owner token was recorded. -
Feature totalPendingSize, totalDelayedSize and totalReservedSize #61231 #
Upstream: “Add Queue `allXSize` methods”
Laravel 13.26.0 18 Aug 2026 · 10 changes
-
Change Promote nested data to top-level managed queue event #61225 also 12.x #
-
Change Promote nested data to top-level managed queue event #61209 #
-
Feature Queue::forward(): send a queue's new dispatches somewhere else #61188 #
Upstream: “Introduce `Queue::forward()`”
Queue::forward('reports', 'reports-slow')redirects a queue by name, optionally to another connection, so every dispatch that namesreportslands onreports-slowwithout touching the code that dispatches it. The operational case is a queue that has become a problem: one tenant's jobs swamping a shared queue, or work that needs different worker settings.It lives in process memory, so it goes in a service provider, and a running worker keeps whatever it booted with: changing it means a deploy and a worker restart. It also covers new dispatches only. Workers resolve the name through the forward too, so jobs already on
reportsstay there, and a worker on--queue=reportswith the forward registered will not pick them up. Drain the old queue first. -
Change Retry timed-out cloud agent long-polls #61178 also 12.x #
-
Feature Queued event listeners can be debounced too #61169 #
Upstream: “Add debounceable queued listeners”
-
Change queue:work prints which queues are paused #61142 #
Upstream: “Surface paused queues in the queue worker output”
A paused queue and an empty queue produced identical worker output, which made a forgotten pause one of the quieter ways to lose a day. The worker now says so, and fires
WorkerQueuePausedandWorkerQueueResumedas it notices. -
Performance Paused-queue checks no longer read across cluster slots #61139 #
Upstream: “Avoid cross-slot reads when checking paused queues”
The pause check ran on every poll of every queue, and on Redis Cluster its keys could land in different slots, making it a cross-slot read on the hot path. Part of the same run of work as the cluster-safety fixes in 13.31.
-
Feature JobReleased fires from the worker #61108 #
Upstream: “Add JobReleased event to Worker”
Laravel 13.25.0 11 Aug 2026 · 8 changes
-
Feature A global pause switch: queue:pause --all #61126 #
Upstream: “Add a global pause switch for queues”
Queue::pauseAll()andQueue::resumeAll(), andphp artisan queue:pause --allin front of them, stop every queue on every connection rather than one named queue at a time. This is the switch you want during a database failover or a bad deploy, where the alternative was pausing queues one by one or stopping the workers and losing the reservations they held.Paused means workers keep running and keep polling but pop nothing, so the backlog builds in Redis rather than in a killed process's memory.
pauseFor(), which takes a TTL so a pause expires on its own instead of outliving the incident and the person who declared it, has been there since 12.40.2, but only for one queue at a time. The global switch has no timed version. -
Fix Backed enum queue names work for queued mailables #61066 #
Upstream: “Respect backed enum queue names when queueing mailables”
Queue names as enums are supported across the queue API, but a mailable queued with one was pushed to a queue named after the enum case object rather than its value — so the mail sat on a queue no worker was listening to, with nothing logged. Worth checking if you moved your queue names to an enum during this cycle.
-
Feature JobTimedOut carries the timeout it exceeded #61060 #
Upstream: “add the timeout to job timedout”
-
Feature UniqueJobSkipped: the silent no-op finally fires an event #61039 #
Upstream: “Add a UniqueJobSkipped event”
A
ShouldBeUniquedispatch that finds the lock held is dropped with no exception, no log line and no failed job — the job simply never appears anywhere, which is the single most confusing behaviour in Laravel's queue.UniqueJobSkippedmakes it observable: listen for it and you can count, log or alert on dispatches that went nowhere.It fires on the
dispatch()path, which is also the only path that takes the lock —Bus::batch(),Queue::push()andqueue:retryskip uniqueness entirely, so they have nothing to report. -
Fix Bulk pushes to the database driver respect after-commit dispatch #60996 #
Upstream: “Respect after-commit dispatch when bulk pushing to DatabaseQueue”
-
Feature Add uuid to queue fake for inspection methods #60966 #
Laravel 13.24.0 4 Aug 2026 · 13 changes
-
Change Revert "Use Str::uuid()->toString() instead of (string) casts (#60995)" #61028 #
-
Change Use strict comparisons for int-to-int checks #61005 #
-
Change Use reduce() instead of foreach accumulator loops #61004 #
-
Change Use Str::uuid()->toString() instead of (string) casts #60995 #
-
Change Sync property docblocks with their parent declarations #60991 #
-
Change Use Stringable::explode() instead of wrapping explode() in a Collection #60979 #
-
Change Prefer isEmpty()/isNotEmpty()/contains() over count() checks #60978 #
-
Change Respect job delays when bulk pushing to FailoverQueue #60950 #
-
Change Use self::SUCCESS/self::FAILURE in console commands #60928 #
-
Change Consolidate Artisan command $name/getArguments()/getOptions() into $signature #60926 #
-
Change Respect job delays when bulk pushing to QueueFake #60916 #
-
Change Use immutable timestamps in batch testing fakes #60912 #
-
Fix Unique locks became owner-checked #60906 #
Upstream: “Attempt at lock fix”
Releasing a unique lock was unconditional: whoever called release cleared the
laravel_unique_job:key for that unique ID, whether or not they were the dispatch that acquired it. WithShouldBeUniqueUntilProcessing, where a second copy can legitimately be queued while the first runs, the first copy finishing released the second copy's lock — and the next dispatch queued a duplicate.UniqueLock::acquire()now stores the lock's owner token on the job asuniqueLockOwnerand the release checks it. Two conditions apply, and both are quiet: the job has to use theQueueabletrait, and the cache store has to implementLockProvider. On a store that does not — the array store, or a custom one — nothing is recorded and the old behaviour stands.
Laravel 13.23.0 27 Jul 2026 · 1 change
-
Fix Reverted the 13.22 ShouldBeUniqueUntilProcessing lock change #60905 #
Upstream: “Revert "Fix ShouldBeUniqueUntilProcessing retries releasing locks they don't …"”
Worth seeing as one story rather than four entries: 13.22 tried to stop retried
ShouldBeUniqueUntilProcessingjobs releasing locks they did not own, 13.23 reverted it, 13.24 solved it differently with an owner token on the lock, and 13.27 closed the after-commit rollback path it had missed. If you pinned below 13.24 during that run, this is the arc you were avoiding.
Laravel 13.22.0 24 Jul 2026 · 5 changes
-
Feature Add multi-queue support to queue:clear command #60873 #
-
Feature add the exception to JobReleasedAfterException #60823 #
-
Feature The #[Delay] attribute is honoured by Bus::batch() and bulk pushes #60766 #
Upstream: “Support `Delay` attribute when using `Bus::batch` & bulk”
-
Change adjust queue fake to handle created at and creationTimeOfOldestPendingJob #60730 #
Laravel 13.21.0 21 Jul 2026 · 5 changes
Laravel 13.20.0 14 Jul 2026 · 6 changes
Laravel 13.19.0 7 Jul 2026 · 6 changes
-
Deprecation Remove deprecated `StaticCallOnNonStaticToInstanceCallRector` fixer rule #60670 #
-
Change Pop managed queue jobs from the cloud-agent instead of SQS #60660 also 12.x #
-
Change Pop managed queue jobs from the cloud-agent instead of SQS #60659 #
-
Performance Bulk SQS dispatch uses SendMessageBatch #60645 #
Upstream: “Send bulk SQS jobs via SendMessageBatch”
Bus::batch()and any other bulk push against SQS sent one API call per job. They now go ten at a time throughSendMessageBatch— an order of magnitude fewer round trips, and an order of magnitude fewer requests on the bill, for a thousand-job batch. -
Feature Support inspecting reserved jobs on the queue fake #60644 #
Laravel 13.18.1 2 Jul 2026 · 2 changes
Laravel 13.18.0 30 Jun 2026 · 4 changes
-
Fix Fix RateLimited middleware not serializing releaseAfter in __sleep() #60609 #
-
Feature WorkerStopping exposes the jobs-processed count and last job timestamp #60592 #
Upstream: “Expose jobs processed count and last job timestamp on the WorkerStopping event”
-
Change DebounceLock reworked #60575 #
Upstream: “13.x-improve-DebounceLock”
Laravel 13.17.0 23 Jun 2026 · 2 changes
Laravel 13.16.0 16 Jun 2026 · 4 changes
Laravel 13.15.0 9 Jun 2026 · 14 changes
-
Internals Add generic to QueueRoutes::all() return type #60447 #
-
Feature Add missing type hints to WorkerIdle and listenForSignals #60389 #
-
Fix Fix FIFO queue name normalization in Cloud managed queues #60316 #
-
Fix Fix queue:failed command to show real class name #60279 also 12.x #
-
Change Throw ManagedQueueNotFoundException when a managed queue is missing #60276 also 12.x #
-
Change wip 4f91e08bd4f4 #
-
Change remove comment 4614124208e5 #
-
Change stop-when-empty-for f6a129544165 #
Laravel 13.14.0 4 Jun 2026 · 2 changes
Laravel 13.13.0 2 Jun 2026 · 5 changes
Laravel 13.12.0 26 May 2026 · 6 changes
Laravel 13.11.0 19 May 2026 · 1 change
Laravel 13.10.0 19 May 2026 · 10 changes
-
Feature Add JSON output option to ListFailedCommand and corresponding tests #60168 #
-
Change Optionally flush the SQS overflow store on queue:clear #60138 #
-
Change Pass WorkerOptions to Pausing/Resuming/Interrupted #60135 #
-
Feature WorkerIdle: the event for a worker with nothing to do #60134 #
Upstream: “Add WorkerIdle event”
Fires when a poll finds every queue empty. The obvious use is scaling — an idle worker is a worker you are paying for — but it is also the cleanest signal that a queue has drained, which is what a deploy gate or a test harness usually wants and previously had to infer from a size of zero it might have read mid-pop.
Laravel 13.9.0 13 May 2026 · 13 changes
-
Change Replace @return with @var on docblocks for properties #60087 #
-
Change Display error in `queue:pause` when `Worker` isn't pausable #60023 #
-
Change Ensure that the named arguments are sorted during a call #60017 #
-
Fix Fix issue using custom aws credential providers #60000 #
-
Feature SQS payloads over 1 MB can overflow to a cache store #59734 #
Upstream: “Add optional disk storage for large SQS queue payloads”
SQS rejects a message over 1 MB, and the failure arrives at dispatch as an SDK exception rather than as anything queue-shaped. With
overflowenabled on the connection, an oversized payload is written to a cache store with a pointer put on the queue instead, and the job reads it back when it runs. The pull request's title says disk, but what shipped is a cache store.13.10 added
flush_on_clear, so clearing a queue no longer strands the payloads it pointed at. It flushes the whole store, so give the overflow a store of its own before turning it on.It remains a rescue rather than a licence: a job whose arguments run to a megabyte is usually a job that should be passed an ID.
Laravel 13.8.0 5 May 2026 · 6 changes
-
Feature The same across every queue: allPendingJobs() and friends #59997 #
Upstream: “Add all* queue inspection methods”
allPendingJobs(),allDelayedJobs()andallReservedJobs()span every queue on the connection. The counting methods followed:totalPendingSize(),totalDelayedSize()andtotalReservedSize()in 13.27 andtotalSize()in 13.31, which answer "how much is queued across everything" without materialising the payloads. -
Feature Add enum support to QueueFake assertPushedOn method #59990 #
-
Feature Worker pausing and resuming events #59895 #
Upstream: “Add Worker Pausing/Resuming events”
-
Change Memoize credentials in SqsConnector #59867 also 12.x #
-
Feature Support named credential providers for SQS queue connections #59754 #
-
Change normalize to string 5cbe7eb2313a #
Laravel 13.7.0 28 Apr 2026 · 9 changes
-
Fix Correct Batch fresh and add @return to self|null #59891 #
-
Feature Add enum support to setDefaultDriver in QueueManager, LogManager, and SessionManager #59861 #
-
Feature WorkerInterrupted #59848 #
Upstream: “Introduce WorkerInterrupted event”
-
Change Update constructor parameter type for limiterName #59841 #
-
Feature Jobs can react to the signals sent to their worker #59833 #
Upstream: “Allow jobs to react to worker signals”
A long-running job could not previously tell that its worker had been asked to stop; it ran to completion or was killed at the supervisor's timeout, mid-flight, with whatever it had already committed left behind. A job can now observe the signal and wind itself down — checkpoint, stop at a batch boundary, release itself cleanly — which is the difference between a deploy costing you a retry and a deploy costing you a half-written import.
-
Fix Fix PendingDispatch resolving Cache for every dispatched job #59821 #
-
Change Allowing `DebounceFor` attribute to be inherited #59795 #
Laravel 13.6.0 21 Apr 2026 · 7 changes
-
Feature Support named credential providers for SQS queue connections #59733 #
-
Change Change attempts column type from tiny to small integer #59718 #
-
Feature Debounceable jobs: #[DebounceFor] #59507 #
Upstream: “Add debounceable queued jobs”
A debounced job waits for a quiet period before it runs, and each new dispatch during that period pushes the deadline back — so a burst of fifty "rebuild this report" dispatches produces one run, after the burst, rather than one run per dispatch or one run at the start.
It is the third member of a family that is easy to confuse.
ShouldBeUniquedrops a duplicate dispatch outright.ShouldBeUniqueUntilProcessinglets one new copy be queued while the first runs.#[DebounceFor]keeps collapsing dispatches until the input stops arriving, which is what you actually want for "recompute when this changes". 13.18 reworked the lock behind it, 13.26 extended it to queued listeners, and 13.27 fixed the max-wait window not restarting after a run.
Laravel 13.5.0 14 Apr 2026 · 6 changes
-
Feature Support #[Delay] attribute on queued mailables #59580 #
-
Fix Fix ShouldBeUniqueUntilProcessing retries releasing locks they don't own #59567 #
-
Change formatting 64a0ff275c68 #
-
Fix fix: fix 8563ff12bc9e #
-
Change feat: redis cluster full support d6be8af77ce3 #
Laravel 13.4.0 7 Apr 2026 · 5 changes
-
Fix Fix `#[WithoutRelations]` queue attribute not being inherited by child classes #59568 #
-
Feature Reading the jobs inside a queue, from the framework #59511 #
Upstream: “Add queue methods to inspect jobs”
pendingJobs(),delayedJobs()andreservedJobs()return collections ofInspectedJob, each with its UUID, name, attempts and creation time; 13.13 added the raw payload and 13.14 the queue name. Before this, seeing what was actually queued meantLRANGEagainst Redis and decoding the payloads by hand.It works on Redis and the database driver. SQS and Beanstalkd cannot list their contents, so there it returns an empty collection rather than an error, which looks just like an empty queue.
-
Fix Fix missing `Illuminate\Queue\Attributes\Delay` attribute #59504 #
Laravel 13.3.0 1 Apr 2026 · 9 changes
-
Performance prefer `isset()` over `in_array()` for better performance #59457 #
-
Change Cache getLockForPopping() result in DatabaseQueue #59435 #
-
Feature Add enum support to QueueManager connection methods #59389 #
-
Fix Fix dependency injection of faked queueing dispatcher #59378 #
-
Change Update shared lock key documentation for clarity #59375 #
-
Change formatting 628e7cb77ad2 #
Laravel 13.2.0 24 Mar 2026 · 5 changes
Laravel 13.1.1 18 Mar 2026 · 1 change
Laravel 13.1.0 18 Mar 2026 · 2 changes
Laravel 13.0.0 17 Mar 2026 · 20 changes
-
Feature BatchCanceled carries the exception that cancelled the batch #59163 #
Upstream: “Add Exception to BatchCanceled event”
-
Internals Add missing @throws into docblock for various methods #59016 #
-
Change Refactor parameter names that are implemented from the interface for consistency #59015 #
-
Breaking JobAttempted carries the exception, not a boolean #58954 #
Upstream: “Ensure SyncQueue JobAttempted gets the actual exception”
JobAttempted::$exceptionOccurred, a boolean, is replaced by$exception, which holds theThrowableornull. A listener still reading the old property now throws anErrorException.This is the event to count attempts from: it fires in a
finallyafter every attempt, including the ones that threw, whichJobProcessedskips. A timeout that kills the worker never reaches it.successful()is false when the attempt threw or failed the job, but true for a job released byRateLimitedorWithoutOverlapping, so check$event->job->isReleased()as well. Now it can also say why without a second listener onJobFailed. -
Breaking The Queue contract gained four size methods #58914 #
Upstream: “add missing methods to Queue interface”
Illuminate\Contracts\Queue\Queuenow declarespendingSize(),delayedSize(),reservedSize()andcreationTimeOfOldestPendingJob()alongside thesize()it always had. Every first-party driver has implemented them since 12.19, when the contract only listed them as@methoddocblocks; any custom queue driver you maintain that does not will fatal on instantiation until you add them.The last of the four is the interesting one. Queue length cannot tell ten thousand fast jobs from one job nobody has picked up in fifteen minutes; the creation time of the oldest pending job can, on Redis and the database driver. SQS and Beanstalkd have no way to answer and return
null. -
Change Queued notifications honour #[DeleteWhenMissingModels] #58908 #
Upstream: “feat: respect DeleteWhenMissingModels attribute on queued notifications”
-
Feature Job configuration as PHP attributes #58578 #
Upstream: “Attributes”
The settings that were properties and methods on a job class became attributes. Ten of them arrived with 13.0 —
#[Tries],#[Backoff],#[Timeout],#[FailOnTimeout],#[MaxExceptions],#[Queue],#[Connection],#[UniqueFor],#[WithoutRelations]and#[DeleteWhenMissingModels]— with#[DebounceFor],#[Delay]and#[CountCrashesAsExceptions]following during the 13.x line, for thirteen in 13.42.Worth knowing for anything that reads job configuration from outside the worker: these are read by
Illuminate\Queue\Attributes\ReadsQueueAttributes, so a tool that only looks for a$triesproperty now sees nothing on a job that sets#[Tries(5)]. -
Feature add dispatchAfterResponse to the Dispatcher Contract #58428 #
-
Feature WorkerStopping says why the worker stopped #58341 #
Upstream: “Add reason to WorkerStopping event”
The event gained a reason, so "the worker exited" stops being a single undifferentiated signal: a graceful
queue:restart, a memory limit, a--max-jobsceiling and a timeout are now told apart from a listener. 13.18 added the jobs-processed count and the timestamp of the last job, 13.30.1 and 13.31 added the connection and queue, and 13.30 started printing the reason inqueue:work's own output. -
Feature Queue routing: default a job class to a queue in one place #58094 #
Upstream: “Add ability to default queue by class type”
Queue::route(ProcessPodcast::class, 'podcasts', 'redis')declares, centrally, which connection and queue a job class belongs on. Until now that decision lived on the job — an$queueproperty or anonQueue()call at every dispatch site — which meant moving one job between queues touched every place that named it.Note the real signature is
route($class, $queue = null, $connection = null); the release notes show it with named arguments, and positionally the queue comes before the connection. It accepts an array of classes, and since 13.15 the queue may be a backed enum. -
Breaking QueueBusy::$connection renamed to $connectionName #56673 #
Upstream: “Make QueueBusy event consistent with other queue events”
Every other queue event calls it
$connectionName;QueueBusycalled it$connection. It now matches. If you listen forQueueBusy— the event behindqueue:monitor, which is how most people alert on backlog — the listener reads a property that no longer exists and will throw. -
Feature Listeners can decide deleteWhenMissingModels at runtime #55508 #
Upstream: “Allow Listeners to dynamically specify deleteWhenMissingModels”
-
Feature Add #[\Override] to the BatchFake class methods #55358 #
Common questions
What changed about queues in Laravel 13?
The headline addition is queue routing — Queue::route() declares which connection and queue a job class defaults to, centrally, instead of at every dispatch site. Alongside it: job configuration as PHP attributes (#[Tries], #[Backoff], #[Timeout], #[FailOnTimeout] and nine more), the queue size methods moved onto the Queue contract, methods that read the jobs inside a queue, and debounceable jobs. Over the 13.x patch releases the largest single theme has been pausing: a global pause switch, worker events, and worker output that distinguishes a paused queue from an empty one.
What breaks in the queue when upgrading from Laravel 12 to 13?
Three things, all low impact unless you maintain a custom queue driver. The Illuminate\Contracts\Queue\Queue contract gained pendingSize(), delayedSize(), reservedSize() and creationTimeOfOldestPendingJob(), which every custom driver must now implement. JobAttempted replaced the boolean $exceptionOccurred with $exception, holding the Throwable or null. And QueueBusy::$connection was renamed $connectionName to match every other queue event, which breaks listeners behind queue:monitor alerting.
How do I pause all Laravel queues at once?
php artisan queue:pause --all, added in Laravel 13.25, or Queue::pauseAll() in code, with Queue::resumeAll() to undo it. Paused workers keep running and keep polling but pop nothing, so the backlog builds in Redis rather than being lost with a killed process, and jobs already running are unaffected. For a single queue, Queue::pauseFor() sets a TTL so the pause expires on its own rather than outliving the incident.
What is #[DebounceFor] and how is it different from ShouldBeUnique?
A debounced job waits for a quiet period before running, and every dispatch during that period pushes the deadline back — so a burst of fifty dispatches produces one run, after the burst. ShouldBeUnique instead drops a duplicate dispatch outright and holds its lock until the job completes; ShouldBeUniqueUntilProcessing releases at the moment a worker starts it. Debouncing is what you want for "recompute when this changes"; uniqueness is what you want for "never queue two of these".
Can I see what is inside a Laravel queue without reading Redis?
Since Laravel 13.4, yes. pendingJobs(), delayedJobs() and reservedJobs() return the jobs as collections, each with its name, attempts and creation time, and since 13.8 allPendingJobs() and friends span every queue on the connection. The counting methods — pendingSize(), delayedSize(), reservedSize(), totalSize() — answer the same questions without materialising anything. Before that, inspecting a backlog meant LRANGE against Redis and decoding payloads by hand. All of this works on Redis and the database driver; on SQS and Beanstalkd the job lists come back empty and totalSize() returns 0.
Why does this changelog list changes the Laravel release notes do not call queue changes?
Because it is assembled from changed file paths rather than from wording. Every commit in every 13.x release is kept if it touched a file that decides how background work behaves — Illuminate\Queue, Illuminate\Bus, the queue contracts, the queue console commands — plus queue-aware files elsewhere, such as the queued mailable that was pushed to the wrong queue when its name was a backed enum. A subject line that never says "queue" is caught anyway.