Skyline

Laravel 13 queue & job changelog

Every change to Laravel's queues, jobs, workers and batches since 13.0, in one place — because they do not survive being read out of the release notes.

v13.0.0 → v13.42.0 · 256 changes · 53 with notes · Updated

RSS Markdown

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 the Queue contract 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 dropped ShouldBeUnique dispatch 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.

#58578, #59507

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.

#58094, #61188, #60402

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.

Where this comes from

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.

Show

Counts below are totals, not filtered totals.

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() or dispatchAfterResponse() 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 with app()->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. With Bus::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 use Exceptions::fake() and Exceptions::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 to failed_jobs but the batch never heard about it. The failure handler unserialized the job to find its batch, hit the same ModelNotFoundException and gave up. The batch kept a failed count of zero, so it was never cancelled, its catch and finally callbacks never ran, and queue:retry-batch could 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, catch runs, and finally runs once every job has run. The job's own failed() 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 as RateLimited or WithoutOverlapping, returned normally, so queue:work printed a green DONE for an attempt that was going to run again. It now prints a yellow RELEASED. A release after an exception still prints a yellow FAIL, and the --json output already said "result":"released", so neither of those changes. Anything that counts DONE lines 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's boot() 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 in DB::whenQueryingForLongerThan(), and a CarbonInterval works too. It is a JobAttempted listener 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. $job is 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 --timeout never will: the worker dies before JobAttempted fires, 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 sync connection JobProcessed::$duration was always null. SyncQueue now times fire() the way the worker does and passes the result to JobProcessed and JobAttempted, so a duration listener gets a number from a queued job run with dispatchSync(). The deferred and background connections run through the same code and get it as well.

    If you subclass SyncQueue, raiseAfterJobEvent() and raiseJobAttemptedEvent() each gained a $duration parameter. 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 in failed_jobs without handle() 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 exception column of failed_jobs for DatabaseQueue->pop(. That attempt never reached handle().

  • 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 threw Call to undefined method, so a test could not fake some jobs and still run the after-response one. After Bus::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:retry unserialises each failed job to read retryUntil() and its queueable options, and unserialising a SerializesModels job reloads its models. One deleted row threw ModelNotFoundException and ended the command. Every job after it stayed in failed_jobs, and running the command again stopped at the same job until someone found it and ran queue:forget. $deleteWhenMissingModels did 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 $deleteWhenMissingModels and otherwise fails it again, so expect that row back in failed_jobs with the ModelNotFoundException as 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”

    JobProcessed only fires when fire() returns, which kept #61672's duration away from the attempts you most wanted timed. The worker now stops the clock in a finally and passes the same milliseconds to JobAttempted::$duration, so one listener can time every attempt that ran, whether it succeeded or threw. JobFailed and JobExceptionOccurred still carry no duration.

    It is null when the job never ran, as when the worker fails it for having used up its attempts before fire(). A timed-out attempt has no duration either: the alarm handler kills the worker first, so JobAttempted never fires. With Worker::$killOnTimeout = false the 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 until tries ran out, which with $tries = 0 was never.

    Now copied like failOnTimeout, as the attribute or the $countCrashesAsExceptions property. It still needs maxExceptions set 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 whose viaConnection() or viaQueue() throws, now releases its lock before the exception leaves Event::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 a ShouldBeUnique job'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 until uniqueFor ran 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=emails lists 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 large failed_jobs table 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 ShouldBeUnique job's lock checked whether the job implemented ShouldBeUnique. A queued listener is wrapped in a CallQueuedListener, 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 firing UniqueJobSkipped — until the lock expired. With no uniqueFor, 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' => true sends 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 = true in a service provider's boot() 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”

    SendQueuedMailable copied tries, timeout and 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 with ModelNotFoundException and landed in failed_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 to JobQueueing and JobQueued, where every other path passes the string. A listener that logs or counts by $event->queue got 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 ShouldBeUnique job pushed inside Queue::fakeFor() or fakeExceptFor() 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 with is(), 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's memory_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=-1 a 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 what php -i says 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. Only tries stopped it, so a job with a high tries for flaky APIs could kill workers over and over.

    Put #[CountCrashesAsExceptions] on the job and the worker writes a job-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 reaches maxExceptions the job is failed before it runs again. It does nothing without maxExceptions set, 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::$restartable set to false, workers ignore the restart signal. queue:restart used 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::$duration is the time spent in the job's fire() — its middleware and handle() — in milliseconds, to two decimal places, timed with the worker's hrtime clock. Recording job runtime used to take a JobProcessing listener to stash a start time and a JobProcessed listener 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 on JobAttempted and #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 Interruptible has heard about SIGTERM, SIGINT and SIGQUIT since 13.7, but not about its own timeout. The timeout handler now calls interrupted(SIGALRM) and fires JobInterrupted first, 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 JobInterrupted and 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=emails deletes 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-batch for 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 Batch the last job was holding, loaded before the repository marked it finished or cancelled. A BatchFinished listener got a batch whose finished() was false and whose finishedAt was null, and a BatchCanceled listener got one whose cancelled() 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::$timedOutExitCode has been settable for years and has never done anything. kill() sends the process an uncatchable SIGKILL just before its exit($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 the SIGKILL and can end the process some other way. Laravel Cloud uses it to pcntl_exec() into sh -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, the SIGKILL happens 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 SIGALRM handler fails or releases the job, fires JobTimedOut, then SIGKILLs its own process and leaves the supervisor to start a new one. Set Worker::$killOnTimeout = false in a service provider and the handler throws the TimeoutExceededException into 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 Throwable swallows 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 Add `isManagedQueue` to CloudManager #61568 #

  • Change use "once" #61526 #

  • Change change unused variables to named arguments #61513 #

  • 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:*, or SCAN on 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 a MULTI that could span nodes, which a cluster refuses — so batch dispatch failed outright on some cluster setups. And allQueueNames() ran KEYS, which now gives way to SCAN on 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

  • Change connection and queue on WorkerStopping #61387 #

Laravel 13.30.0 1 Sep 2026 · 4 changes

  • Change Update QueueManager.php #61367 #

  • Feature Add totalXSize methods to Cloud Queue #61352 #

  • Fix Fix incorrect $startTime type in Worker::stopIfNecessary docblock #61347 #

  • Change queue:work prints why the worker stopped #61339 #

    Upstream: “Output the worker stop reason in queue:work”

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 ShouldBeUnique job 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 names reports lands on reports-slow without 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 reports stay there, and a worker on --queue=reports with 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 #

  • Change Retry timed-out cloud agent long-polls #61177 #

  • Feature Queued event listeners can be debounced too #61169 #

    Upstream: “Add debounceable queued listeners”

  • Feature Add managedQueues() to the Cloud queue #61149 #

  • 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 WorkerQueuePaused and WorkerQueueResumed as 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() and Queue::resumeAll(), and php artisan queue:pause --all in 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.

  • Change adjust queues to allow for enums #61116 #

  • Feature Support fail on timeout for notifications #61072 #

  • 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 ShouldBeUnique dispatch 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. UniqueJobSkipped makes 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() and queue:retry skip 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. With ShouldBeUniqueUntilProcessing, 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 as uniqueLockOwner and the release checks it. Two conditions apply, and both are quiet: the job has to use the Queueable trait, and the cache store has to implement LockProvider. 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 ShouldBeUniqueUntilProcessing jobs 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 #

  • Fix fix queue name parameter in Mailer::later() #60865 #

  • 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

  • Change Apply rector PHPUnit assertion cleanups #60807 #

  • Change Use strict string comparisons #60805 #

  • Change InvalidPayloadException names the job and the queue #60799 #

    Upstream: “Include job name and queue in InvalidPayloadException message”

  • Fix fix: incorrect parameter name in raiseJobAttemptedEvent docblock #60791 #

  • Fix Fix typos in doc blocks #60782 #

Laravel 13.20.0 14 Jul 2026 · 6 changes

  • Internals Reset fake time globally after each test, drop redundant Carbon::setTestNow() cleanup #60761 #

  • Change Optimize by using contains or doesntContain #60745 #

  • Change Enum as queue overlap key #60722 also 12.x #

  • Fix Fix SQS queue size null coalescing precedence #60702 #

  • Change `beforePushing` and `afterPushing` methods on `QueueFake` class #60689 #

  • Performance Add memory usage to WorkerStopping #60613 #

Laravel 13.19.0 7 Jul 2026 · 6 changes

  • Change Update Queue.php #60679 #

  • 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 through SendMessageBatch — 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

  • Feature Support inspecting delayed jobs on the queue fake #60636 #

  • Feature Add `Release` queue middleware #60630 #

Laravel 13.18.0 30 Jun 2026 · 4 changes

  • Fix Fix RateLimited middleware not serializing releaseAfter in __sleep() #60609 #

  • Change Update Worker.php #60608 #

  • 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

  • Change Reduce cache hits when debouncing with maxWait #60559 #

  • Change Should Not Retry Exception Handler #60552 #

Laravel 13.16.0 16 Jun 2026 · 4 changes

  • Feature Add missing @throws \JsonException annotations #60514 #

  • Change Improve return types for callback-passthrough interfaces and helpers #60513 #

  • Fix Fix Batchable batching state checks #60511 #

  • Change Improve return types for the remaining callback-passthrough helpers #60500 #

Laravel 13.15.0 9 Jun 2026 · 14 changes

  • Internals Add generic to QueueRoutes::all() return type #60447 #

  • Internals 13.x-add-unit-docblock-to-attrs #60431 #

  • Change 13.x-add-prohibited-to-more #60430 #

  • Feature Allow enums in Queue::route #60402 #

  • 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 Dedicated Cloud Queue #60181 also 12.x #

  • Change Back port cloud queues #60122 also 12.x #

  • Change Backport #60000 to 12.x #60006 #

  • Change wip 4f91e08bd4f4 #

  • Change remove comment 4614124208e5 #

  • Change stop-when-empty-for f6a129544165 #

Laravel 13.14.0 4 Jun 2026 · 2 changes

  • Change Update DebounceFor.php #60388 #

  • Feature Add queue to InspectedJob #60374 #

Laravel 13.13.0 2 Jun 2026 · 5 changes

  • Fix fix: Queue and Connection attributes in Mailable::queue() and later() #60328 #

  • Feature Add payload to InspectedJob #60326 #

  • Fix Fix FIFO queue name normalization in Cloud managed queues #60315 #

  • Feature Bus::bulk(): push many jobs at once, grouped by connection and queue #60297 #

    Upstream: “Introduce Bus::dispatchBulk()”

  • Change hint the unit #60289 #

Laravel 13.12.0 26 May 2026 · 6 changes

  • Change Throw ManagedQueueNotFoundException when a managed queue is missing #60275 #

  • Change remove compact #60234 #

  • Change Update ClearCommand.php #60215 #

  • Change Update Worker.php #60201 #

  • Change 13.x default clear() queue param to null #60192 #

  • Change use display name 2627847779c7 #

Laravel 13.11.0 19 May 2026 · 1 change

  • Change Dedicated Cloud Queue #60180 #

Laravel 13.10.0 19 May 2026 · 10 changes

  • Change Stop when empty for #60176 #

  • Feature Add JSON output option to ListFailedCommand and corresponding tests #60168 #

  • Feature Support enum queue names in QueueFake #60161 #

  • Change 13.x-workeroptions-looping #60153 #

  • Feature Add `assertPushedOnce()` #60150 #

  • Change Update Worker.php #60145 #

  • 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.

  • Change 13.x-optimize-queue-pause #60109 #

Laravel 13.9.0 13 May 2026 · 13 changes

  • Feature allow closure for ThrottlesExceptions #60103 #

  • Change Use config for queue suffix and prefix #60094 #

  • Change Replace @return with @var on docblocks for properties #60087 #

  • Feature Support `after_commit` for queue metrics #60078 #

  • Change Cloud queue metrics #60074 #

  • Performance add index for database performance #60073 #

  • Change 13.x-custom-timedout-exit #60072 #

  • Change Make PendingDispatch conditionable #60047 #

  • 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 Add Preparable interface for Jobs #59879 #

  • 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 overflow enabled 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() and allReservedJobs() span every queue on the connection. The counting methods followed: totalPendingSize(), totalDelayedSize() and totalReservedSize() in 13.27 and totalSize() 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 #

  • Change Memoize credentials in SqsConnector #59866 #

  • 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 #

  • Change Apply rector fixes #59787 #

Laravel 13.6.0 21 Apr 2026 · 7 changes

  • Change Normalize Carbon #59750 #

  • Feature Support named credential providers for SQS queue connections #59733 #

  • Change Change attempts column type from tiny to small integer #59718 #

  • Change wip #59711 #

  • Change Enforce static calls #59704 #

  • Change Remove unused variable in `catch()` #59689 #

  • 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. ShouldBeUnique drops a duplicate dispatch outright. ShouldBeUniqueUntilProcessing lets 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

  • Change Refactor `Skip` middleware #59651 #

  • 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 #

  • Change Use #[Delay] attribute in Bus Dispatcher #59514 #

  • Change Use Delay attribute in NotificationSender #59513 #

  • Feature Reading the jobs inside a queue, from the framework #59511 #

    Upstream: “Add queue methods to inspect jobs”

    pendingJobs(), delayedJobs() and reservedJobs() return collections of InspectedJob, 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 meant LRANGE against 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

  • Feature Add BatchStarted event #59458 #

  • 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 #

  • Change 13.x-show-mem-usage-in-verbose #59379 #

  • Fix Fix dependency injection of faked queueing dispatcher #59378 #

  • Change Update shared lock key documentation for clarity #59375 #

  • Feature Add lost connection to WorkerStopReason #59370 #

  • Change formatting 628e7cb77ad2 #

Laravel 13.2.0 24 Mar 2026 · 5 changes

  • Feature allow vaerdic for backoff #59354 #

  • Change 13.x-timedout-worker--stop-reason #59310 #

  • Feature Allow opting out of worker Job exception reporting #59308 #

  • Feature Allow for passing enums to attributes #59297 #

  • Change feat(queue): support enums in `#[Queue]` and `#[Connection]` #59278 #

Laravel 13.1.1 18 Mar 2026 · 1 change

  • Change Break queue dependency #59275 #

Laravel 13.1.0 18 Mar 2026 · 2 changes

  • Change Use Carbon::now() and Date::now() instead of now() helper #59252 #

  • Fix Fix: Batch::add() wipes queue assignment on first job in array chains #59233 #

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”

  • Change Update MonitorCommand.php #59151 #

  • Fix fix/queue-routes-docblocks #59119 #

  • Change adjust and tests #59118 #

  • 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 the Throwable or null. A listener still reading the old property now throws an ErrorException.

    This is the event to count attempts from: it fires in a finally after every attempt, including the ones that threw, which JobProcessed skips. 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 by RateLimited or WithoutOverlapping, so check $event->job->isReleased() as well. Now it can also say why without a second listener on JobFailed.

  • Breaking The Queue contract gained four size methods #58914 #

    Upstream: “add missing methods to Queue interface”

    Illuminate\Contracts\Queue\Queue now declares pendingSize(), delayedSize(), reservedSize() and creationTimeOfOldestPendingJob() alongside the size() it always had. Every first-party driver has implemented them since 12.19, when the contract only listed them as @method docblocks; 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 $tries property now sees nothing on a job that sets #[Tries(5)].

  • Change feat: restore eager-loaded relations #58477 #

  • Internals Add missing chain method to interface #58429 #

  • Feature add dispatchAfterResponse to the Dispatcher Contract #58428 #

  • Feature Add starting to Monitor Contract #58399 #

  • 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-jobs ceiling 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 in queue: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 $queue property or an onQueue() 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; QueueBusy called it $connection. It now matches. If you listen for QueueBusy — the event behind queue:monitor, which is how most people alert on backlog — the listener reads a property that no longer exists and will throw.

  • Change pass exception #56148 #

  • 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.

The framework tells you what is in the queue. Something still has to show you.

Skyline is a drop-in replacement for Laravel Horizon. From its dashboard you can pause a single queue, read and search the jobs inside one, run a delayed job now, and see the unique locks that are being held along with the dispatches each one skipped. $139 once, for every app you run it on.

Buy Skyline — $139 once

Secure checkout by Anystack. 30 days to change your mind.