# Laravel 13 queue & job changelog

> Every change to Laravel 13 queues, jobs, workers and batches, collected from the framework's own history with notes on what matters.

Source: https://boring-observability.dev/laravel/queue-changelog
Section: Reference — Boring Observability
Covers: laravel/framework v13.0.0 through v13.33.0
Entries: 213 (26 with notes)
Updated: 2026-09-22

---

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.** Twelve of them by 13.33 — `#[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.** Pending, delayed and reserved counts, the payloads themselves as collections, a single `totalSize()`, and the creation time of the oldest pending job. Four of those are on the `Queue` contract, which is what breaks custom drivers.
- **Pausing grew up.** A global switch, a pause with a TTL, events for workers entering and leaving the paused state, and 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](#pr-58954) |
| **The Queue contract gained four size methods** | 13.0.0 | [Read the note](#pr-58914) |
| **QueueBusy::$connection renamed to $connectionName** | 13.0.0 | [Read the note](#pr-56673) |

## The changes that change how you run queues

Most of the 213 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; 13.x turned it into something you would reach for during an incident. There is a global switch now — `queue:pause --all`, `Queue::pauseAll()` — a pause with a TTL so it expires on its own, 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](#pr-61126), [#61142](#pr-61142), [#59895](#pr-59895), [#61139](#pr-61139)

### The framework will now tell you what is in a queue

Laravel 13 went from `size()` to a full inspection API: pending, delayed and reserved counts per queue, the same across a whole connection, a single `totalSize()`, the payloads themselves as collections, and the creation time of the oldest pending job — the one number that can distinguish a busy queue from a stuck one.

This is the change with the widest blast radius in the major, because the four new methods are on the `Queue` contract. Every custom driver has to implement them.

[#59997](#pr-59997), [#61231](#pr-61231), [#61373](#pr-61373), [#58914](#pr-58914)

### Job configuration moved to attributes

Twelve 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](#pr-58578), [#59507](#pr-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.33. 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. Jobs can also see the signals sent to their worker and wind themselves down rather than being killed mid-flight.

[#60134](#pr-60134), [#61108](#pr-61108), [#61412](#pr-61412), [#58341](#pr-58341), [#59833](#pr-59833), [#61039](#pr-61039)

### Routing, forwarding, and moving work without a deploy

`Queue::route()` declares which queue a job class belongs on in one place rather than at every dispatch site, and `Queue::forward()` redirects an entire queue's traffic elsewhere at runtime. Both exist for the same reason: moving work between queues used to be a code change, and incidents do not wait for a deploy.

[#58094](#pr-58094), [#61188](#pr-61188), [#60402](#pr-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](https://github.com/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

### Laravel 13.33.0 — 2026-09-22

- **[fix]** queue:retry-batch skips bad batch IDs and exits non-zero — [#61653](https://github.com/laravel/framework/pull/61653)

  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](https://github.com/laravel/framework/pull/61644)

  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](https://github.com/laravel/framework/pull/61622)

  `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](https://github.com/laravel/framework/pull/61591)

  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 `SIGKILL`s 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](https://github.com/laravel/framework/pull/61590)
- **[change]** Laravel Cloud: queue:retry pages through failed jobs, with backoff — [#61584](https://github.com/laravel/framework/pull/61584)

### Laravel 13.32.0 — 2026-09-15

- **[feature]** Add `isManagedQueue` to CloudManager — [#61568](https://github.com/laravel/framework/pull/61568)
- **[change]** use "once" — [#61526](https://github.com/laravel/framework/pull/61526)
- **[change]** change unused variables to named arguments — [#61513](https://github.com/laravel/framework/pull/61513)
- **[feature]** Enums accepted by the pause and resume methods — [#61464](https://github.com/laravel/framework/pull/61464)
- **[change]** assign message group id when using fifo — [5c6b069aab23](https://github.com/laravel/framework/commit/5c6b069aab234b9264155335678421ec7746d6e4)

### Laravel 13.31.0 — 2026-09-08

- **[fix]** Fix RateLimited job middleware hitting limits that did not block the job — [#61449](https://github.com/laravel/framework/pull/61449)
- **[feature]** JobInterrupted — [#61412](https://github.com/laravel/framework/pull/61412)
- **[change]** Include connection and queue on WorkerStopping when worker is killed — [#61408](https://github.com/laravel/framework/pull/61408)
- **[feature]** totalSize(): everything queued on a connection, in one number — [#61373](https://github.com/laravel/framework/pull/61373)
- **[fix]** The Redis driver is cluster-safe — [#61198](https://github.com/laravel/framework/pull/61198)

  Two problems on Redis Cluster. `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()` now uses `KEYS`, which is cluster-routable where the previous approach was not.
  
  `KEYS` 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](https://github.com/laravel/framework/commit/ce2511a6ab5ebee4351c4ea9c406eb15eec06f9d)

### Laravel 13.30.1 — 2026-09-01

- **[change]** connection and queue on WorkerStopping — [#61387](https://github.com/laravel/framework/pull/61387)

### Laravel 13.30.0 — 2026-09-01

- **[change]** Update QueueManager.php — [#61367](https://github.com/laravel/framework/pull/61367)
- **[feature]** Add totalXSize methods to Cloud Queue — [#61352](https://github.com/laravel/framework/pull/61352)
- **[fix]** Fix incorrect $startTime type in Worker::stopIfNecessary docblock — [#61347](https://github.com/laravel/framework/pull/61347)
- **[change]** queue:work prints why the worker stopped — [#61339](https://github.com/laravel/framework/pull/61339)

### Laravel 13.27.0 — 2026-08-25

- **[change]** Share cached AWS credentials across processes for SQS queue connections — [#61321](https://github.com/laravel/framework/pull/61321)
- **[fix]** The debounce max-wait window restarts after a run — [#61281](https://github.com/laravel/framework/pull/61281)
- **[fix]** The after-commit rollback path stopped releasing locks it never took — [#61234](https://github.com/laravel/framework/pull/61234)

  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]** allPendingSize, allDelayedSize and allReservedSize — [#61231](https://github.com/laravel/framework/pull/61231)

### Laravel 13.26.0 — 2026-08-18

- **[change]** Promote nested data to top-level managed queue event — [#61225](https://github.com/laravel/framework/pull/61225)
- **[change]** Promote nested data to top-level managed queue event — [#61209](https://github.com/laravel/framework/pull/61209)
- **[feature]** Queue::forward(): move everything on one queue to another — [#61188](https://github.com/laravel/framework/pull/61188)

  `Queue::forward('reports', 'reports-slow')` redirects a queue's traffic to another queue, optionally on another connection. The operational case is a queue that has become a problem — one tenant's jobs swamping a shared queue, or work that needs to be drained somewhere with different worker settings — where the alternative was a deploy.

- **[change]** Retry timed-out cloud agent long-polls — [#61178](https://github.com/laravel/framework/pull/61178)
- **[change]** Retry timed-out cloud agent long-polls — [#61177](https://github.com/laravel/framework/pull/61177)
- **[feature]** Queued event listeners can be debounced too — [#61169](https://github.com/laravel/framework/pull/61169)
- **[feature]** Add managedQueues() to the Cloud queue — [#61149](https://github.com/laravel/framework/pull/61149)
- **[change]** queue:work prints which queues are paused — [#61142](https://github.com/laravel/framework/pull/61142)

  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](https://github.com/laravel/framework/pull/61139)

  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](https://github.com/laravel/framework/pull/61108)

### Laravel 13.25.0 — 2026-08-11

- **[feature]** A global pause switch: queue:pause --all — [#61126](https://github.com/laravel/framework/pull/61126)

  `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. 13.10 also added `pauseFor()`, which takes a TTL so a pause expires on its own instead of outliving the incident and the person who declared it.

- **[change]** adjust queues to allow for enums — [#61116](https://github.com/laravel/framework/pull/61116)
- **[feature]** Support fail on timeout for notifications — [#61072](https://github.com/laravel/framework/pull/61072)
- **[fix]** Backed enum queue names work for queued mailables — [#61066](https://github.com/laravel/framework/pull/61066)

  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](https://github.com/laravel/framework/pull/61060)
- **[feature]** UniqueJobSkipped: the silent no-op finally fires an event — [#61039](https://github.com/laravel/framework/pull/61039)

  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](https://github.com/laravel/framework/pull/60996)
- **[feature]** Add uuid to queue fake for inspection methods — [#60966](https://github.com/laravel/framework/pull/60966)

### Laravel 13.24.0 — 2026-08-04

- **[change]** Revert "Use Str::uuid()->toString() instead of (string) casts (#60995)" — [#61028](https://github.com/laravel/framework/pull/61028)
- **[change]** Use strict comparisons for int-to-int checks — [#61005](https://github.com/laravel/framework/pull/61005)
- **[change]** Use reduce() instead of foreach accumulator loops — [#61004](https://github.com/laravel/framework/pull/61004)
- **[change]** Use Str::uuid()->toString() instead of (string) casts — [#60995](https://github.com/laravel/framework/pull/60995)
- **[change]** Sync property docblocks with their parent declarations — [#60991](https://github.com/laravel/framework/pull/60991)
- **[change]** Use Stringable::explode() instead of wrapping explode() in a Collection — [#60979](https://github.com/laravel/framework/pull/60979)
- **[change]** Prefer isEmpty()/isNotEmpty()/contains() over count() checks — [#60978](https://github.com/laravel/framework/pull/60978)
- **[change]** Respect job delays when bulk pushing to FailoverQueue — [#60950](https://github.com/laravel/framework/pull/60950)
- **[change]** Use self::SUCCESS/self::FAILURE in console commands — [#60928](https://github.com/laravel/framework/pull/60928)
- **[change]** Consolidate Artisan command $name/getArguments()/getOptions() into $signature — [#60926](https://github.com/laravel/framework/pull/60926)
- **[change]** Respect job delays when bulk pushing to QueueFake — [#60916](https://github.com/laravel/framework/pull/60916)
- **[change]** Use immutable timestamps in batch testing fakes — [#60912](https://github.com/laravel/framework/pull/60912)
- **[fix]** Unique locks became owner-checked — [#60906](https://github.com/laravel/framework/pull/60906)

  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 — 2026-07-27

- **[fix]** Reverted the 13.22 ShouldBeUniqueUntilProcessing lock change — [#60905](https://github.com/laravel/framework/pull/60905)

  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 — 2026-07-24

- **[feature]** Add multi-queue support to queue:clear command — [#60873](https://github.com/laravel/framework/pull/60873)
- **[fix]** fix queue name parameter in Mailer::later() — [#60865](https://github.com/laravel/framework/pull/60865)
- **[feature]** add the exception to JobReleasedAfterException — [#60823](https://github.com/laravel/framework/pull/60823)
- **[feature]** The #[Delay] attribute is honoured by Bus::batch() and bulk pushes — [#60766](https://github.com/laravel/framework/pull/60766)
- **[change]** adjust queue fake to handle created at and creationTimeOfOldestPendingJob — [#60730](https://github.com/laravel/framework/pull/60730)

### Laravel 13.21.0 — 2026-07-21

- **[change]** Apply rector PHPUnit assertion cleanups — [#60807](https://github.com/laravel/framework/pull/60807)
- **[change]** Use strict string comparisons — [#60805](https://github.com/laravel/framework/pull/60805)
- **[change]** InvalidPayloadException names the job and the queue — [#60799](https://github.com/laravel/framework/pull/60799)
- **[fix]** fix: incorrect parameter name in raiseJobAttemptedEvent docblock — [#60791](https://github.com/laravel/framework/pull/60791)
- **[fix]** Fix typos in doc blocks — [#60782](https://github.com/laravel/framework/pull/60782)

### Laravel 13.20.0 — 2026-07-14

- **[dx]** Reset fake time globally after each test, drop redundant Carbon::setTestNow() cleanup — [#60761](https://github.com/laravel/framework/pull/60761)
- **[change]** Optimize by using contains or doesntContain — [#60745](https://github.com/laravel/framework/pull/60745)
- **[change]** Enum as queue overlap key — [#60722](https://github.com/laravel/framework/pull/60722)
- **[fix]** Fix SQS queue size null coalescing precedence — [#60702](https://github.com/laravel/framework/pull/60702)
- **[change]** `beforePushing` and `afterPushing` methods on `QueueFake` class — [#60689](https://github.com/laravel/framework/pull/60689)
- **[performance]** Add memory usage to WorkerStopping — [#60613](https://github.com/laravel/framework/pull/60613)

### Laravel 13.19.0 — 2026-07-07

- **[change]** Update Queue.php — [#60679](https://github.com/laravel/framework/pull/60679)
- **[deprecation]** Remove deprecated `StaticCallOnNonStaticToInstanceCallRector` fixer rule — [#60670](https://github.com/laravel/framework/pull/60670)
- **[change]** Pop managed queue jobs from the cloud-agent instead of SQS — [#60660](https://github.com/laravel/framework/pull/60660)
- **[change]** Pop managed queue jobs from the cloud-agent instead of SQS — [#60659](https://github.com/laravel/framework/pull/60659)
- **[performance]** Bulk SQS dispatch uses SendMessageBatch — [#60645](https://github.com/laravel/framework/pull/60645)

  `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](https://github.com/laravel/framework/pull/60644)

### Laravel 13.18.1 — 2026-07-02

- **[feature]** Support inspecting delayed jobs on the queue fake — [#60636](https://github.com/laravel/framework/pull/60636)
- **[feature]** Add `Release` queue middleware — [#60630](https://github.com/laravel/framework/pull/60630)

### Laravel 13.18.0 — 2026-06-30

- **[fix]** Fix RateLimited middleware not serializing releaseAfter in __sleep() — [#60609](https://github.com/laravel/framework/pull/60609)
- **[change]** Update Worker.php — [#60608](https://github.com/laravel/framework/pull/60608)
- **[feature]** WorkerStopping exposes the jobs-processed count and last job timestamp — [#60592](https://github.com/laravel/framework/pull/60592)
- **[change]** DebounceLock reworked — [#60575](https://github.com/laravel/framework/pull/60575)

### Laravel 13.17.0 — 2026-06-23

- **[change]** Reduce cache hits when debouncing with maxWait — [#60559](https://github.com/laravel/framework/pull/60559)
- **[change]** Should Not Retry Exception Handler — [#60552](https://github.com/laravel/framework/pull/60552)

### Laravel 13.16.0 — 2026-06-16

- **[feature]** Add missing @throws \JsonException annotations — [#60514](https://github.com/laravel/framework/pull/60514)
- **[change]** Improve return types for callback-passthrough interfaces and helpers — [#60513](https://github.com/laravel/framework/pull/60513)
- **[fix]** Fix Batchable batching state checks — [#60511](https://github.com/laravel/framework/pull/60511)
- **[change]** Improve return types for the remaining callback-passthrough helpers — [#60500](https://github.com/laravel/framework/pull/60500)

### Laravel 13.15.0 — 2026-06-09

- **[dx]** Add generic to QueueRoutes::all() return type — [#60447](https://github.com/laravel/framework/pull/60447)
- **[dx]** 13.x-add-unit-docblock-to-attrs — [#60431](https://github.com/laravel/framework/pull/60431)
- **[change]** 13.x-add-prohibited-to-more — [#60430](https://github.com/laravel/framework/pull/60430)
- **[feature]** Allow enums in Queue::route — [#60402](https://github.com/laravel/framework/pull/60402)
- **[feature]** Add missing type hints to WorkerIdle and listenForSignals — [#60389](https://github.com/laravel/framework/pull/60389)
- **[fix]** Fix FIFO queue name normalization in Cloud managed queues — [#60316](https://github.com/laravel/framework/pull/60316)
- **[fix]** Fix queue:failed command to show real class name — [#60279](https://github.com/laravel/framework/pull/60279)
- **[change]** Throw ManagedQueueNotFoundException when a managed queue is missing — [#60276](https://github.com/laravel/framework/pull/60276)
- **[change]** Dedicated Cloud Queue — [#60181](https://github.com/laravel/framework/pull/60181)
- **[change]** Back port cloud queues — [#60122](https://github.com/laravel/framework/pull/60122)
- **[change]** Backport #60000 to 12.x — [#60006](https://github.com/laravel/framework/pull/60006)
- **[change]** wip — [4f91e08bd4f4](https://github.com/laravel/framework/commit/4f91e08bd4f4d38b62274ce42e485e4d0cbe0826)
- **[change]** remove comment — [4614124208e5](https://github.com/laravel/framework/commit/4614124208e59ef97f64b06c06676575f5cbdc22)
- **[change]** stop-when-empty-for — [f6a129544165](https://github.com/laravel/framework/commit/f6a1295441655b9edc5913115aa39c0b2aee2e1b)

### Laravel 13.14.0 — 2026-06-04

- **[change]** Update DebounceFor.php — [#60388](https://github.com/laravel/framework/pull/60388)
- **[feature]** Add queue to InspectedJob — [#60374](https://github.com/laravel/framework/pull/60374)

### Laravel 13.13.0 — 2026-06-02

- **[fix]** fix: Queue and Connection attributes in Mailable::queue() and later() — [#60328](https://github.com/laravel/framework/pull/60328)
- **[feature]** Add payload to InspectedJob — [#60326](https://github.com/laravel/framework/pull/60326)
- **[fix]** Fix FIFO queue name normalization in Cloud managed queues — [#60315](https://github.com/laravel/framework/pull/60315)
- **[feature]** Bus::dispatchBulk() — [#60297](https://github.com/laravel/framework/pull/60297)
- **[change]** hint the unit — [#60289](https://github.com/laravel/framework/pull/60289)

### Laravel 13.12.0 — 2026-05-26

- **[change]** Throw ManagedQueueNotFoundException when a managed queue is missing — [#60275](https://github.com/laravel/framework/pull/60275)
- **[change]** remove compact — [#60234](https://github.com/laravel/framework/pull/60234)
- **[change]** Update ClearCommand.php — [#60215](https://github.com/laravel/framework/pull/60215)
- **[change]** Update Worker.php — [#60201](https://github.com/laravel/framework/pull/60201)
- **[change]** 13.x default clear() queue param to null — [#60192](https://github.com/laravel/framework/pull/60192)
- **[change]** use display name — [2627847779c7](https://github.com/laravel/framework/commit/2627847779c74d48b372ff25b792ab5754c57137)

### Laravel 13.11.0 — 2026-05-19

- **[change]** Dedicated Cloud Queue — [#60180](https://github.com/laravel/framework/pull/60180)

### Laravel 13.10.0 — 2026-05-19

- **[change]** Stop when empty for — [#60176](https://github.com/laravel/framework/pull/60176)
- **[feature]** Add JSON output option to ListFailedCommand and corresponding tests — [#60168](https://github.com/laravel/framework/pull/60168)
- **[feature]** Support enum queue names in QueueFake — [#60161](https://github.com/laravel/framework/pull/60161)
- **[change]** 13.x-workeroptions-looping — [#60153](https://github.com/laravel/framework/pull/60153)
- **[feature]** Add `assertPushedOnce()` — [#60150](https://github.com/laravel/framework/pull/60150)
- **[change]** Update Worker.php — [#60145](https://github.com/laravel/framework/pull/60145)
- **[change]** Optionally flush the SQS overflow store on queue:clear — [#60138](https://github.com/laravel/framework/pull/60138)
- **[change]** Pass WorkerOptions to Pausing/Resuming/Interrupted — [#60135](https://github.com/laravel/framework/pull/60135)
- **[feature]** WorkerIdle: the event for a worker with nothing to do — [#60134](https://github.com/laravel/framework/pull/60134)

  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](https://github.com/laravel/framework/pull/60109)

### Laravel 13.9.0 — 2026-05-13

- **[feature]** allow closure for ThrottlesExceptions — [#60103](https://github.com/laravel/framework/pull/60103)
- **[change]** Use config for queue suffix and prefix — [#60094](https://github.com/laravel/framework/pull/60094)
- **[change]** Replace @return with @var on docblocks for properties — [#60087](https://github.com/laravel/framework/pull/60087)
- **[feature]** Support `after_commit` for queue metrics — [#60078](https://github.com/laravel/framework/pull/60078)
- **[change]** Cloud queue metrics — [#60074](https://github.com/laravel/framework/pull/60074)
- **[performance]** add index for database performance — [#60073](https://github.com/laravel/framework/pull/60073)
- **[change]** 13.x-custom-timedout-exit — [#60072](https://github.com/laravel/framework/pull/60072)
- **[change]** Make PendingDispatch conditionable — [#60047](https://github.com/laravel/framework/pull/60047)
- **[change]** Display error in `queue:pause` when `Worker` isn't pausable — [#60023](https://github.com/laravel/framework/pull/60023)
- **[change]** Ensure that the named arguments are sorted during a call — [#60017](https://github.com/laravel/framework/pull/60017)
- **[fix]** Fix issue using custom aws credential providers — [#60000](https://github.com/laravel/framework/pull/60000)
- **[feature]** Add Preparable interface for Jobs — [#59879](https://github.com/laravel/framework/pull/59879)
- **[feature]** SQS payloads larger than 256KB can spill to disk — [#59734](https://github.com/laravel/framework/pull/59734)

  SQS rejects a message over 256KB, and the failure arrives at dispatch as an SDK exception rather than as anything queue-shaped. An oversized payload can now be written to a filesystem disk with a pointer put on the queue instead. 13.10 added flushing that overflow store on `queue:clear`, so clearing a queue no longer strands the payloads it pointed at.
  
  It remains a rescue rather than a licence: a job whose arguments run to a quarter of a megabyte is usually a job that should be passed an ID.


### Laravel 13.8.0 — 2026-05-05

- **[feature]** Reading the jobs inside a queue, from the framework — [#59997](https://github.com/laravel/framework/pull/59997)

  `pendingJobs()`, `delayedJobs()` and `reservedJobs()` return the payloads themselves as collections, with `allPendingJobs()` and friends spanning every queue on the connection. Before this, seeing what was actually queued meant `LRANGE` against Redis and unserializing the payloads by hand.
  
  It is the foundation for the counting methods that followed: `allXSize()` 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](https://github.com/laravel/framework/pull/59990)
- **[feature]** Worker pausing and resuming events — [#59895](https://github.com/laravel/framework/pull/59895)
- **[change]** Memoize credentials in SqsConnector — [#59867](https://github.com/laravel/framework/pull/59867)
- **[feature]** Support named credential providers for SQS queue connections — [#59754](https://github.com/laravel/framework/pull/59754)
- **[change]** normalize to string — [5cbe7eb2313a](https://github.com/laravel/framework/commit/5cbe7eb2313a9a8ecf211e39917d7ecc8d7d8951)

### Laravel 13.7.0 — 2026-04-28

- **[fix]** Correct Batch fresh and add @return to self|null — [#59891](https://github.com/laravel/framework/pull/59891)
- **[change]** Memoize credentials in SqsConnector — [#59866](https://github.com/laravel/framework/pull/59866)
- **[feature]** Add enum support to setDefaultDriver in QueueManager, LogManager, and SessionManager — [#59861](https://github.com/laravel/framework/pull/59861)
- **[feature]** WorkerInterrupted — [#59848](https://github.com/laravel/framework/pull/59848)
- **[change]** Update constructor parameter type for limiterName — [#59841](https://github.com/laravel/framework/pull/59841)
- **[feature]** Jobs can react to the signals sent to their worker — [#59833](https://github.com/laravel/framework/pull/59833)

  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](https://github.com/laravel/framework/pull/59821)
- **[change]** Allowing `DebounceFor` attribute to be inherited — [#59795](https://github.com/laravel/framework/pull/59795)
- **[change]** Apply rector fixes — [#59787](https://github.com/laravel/framework/pull/59787)

### Laravel 13.6.0 — 2026-04-21

- **[change]** Normalize Carbon — [#59750](https://github.com/laravel/framework/pull/59750)
- **[feature]** Support named credential providers for SQS queue connections — [#59733](https://github.com/laravel/framework/pull/59733)
- **[change]** Change attempts column type from tiny to small integer — [#59718](https://github.com/laravel/framework/pull/59718)
- **[change]** wip — [#59711](https://github.com/laravel/framework/pull/59711)
- **[change]** Enforce static calls — [#59704](https://github.com/laravel/framework/pull/59704)
- **[change]** Remove unused variable in `catch()` — [#59689](https://github.com/laravel/framework/pull/59689)
- **[feature]** Debounceable jobs: #[DebounceFor] — [#59507](https://github.com/laravel/framework/pull/59507)

  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 — 2026-04-14

- **[change]** Refactor `Skip` middleware — [#59651](https://github.com/laravel/framework/pull/59651)
- **[feature]** Support #[Delay] attribute on queued mailables — [#59580](https://github.com/laravel/framework/pull/59580)
- **[fix]** Fix ShouldBeUniqueUntilProcessing retries releasing locks they don't own — [#59567](https://github.com/laravel/framework/pull/59567)
- **[change]** formatting — [64a0ff275c68](https://github.com/laravel/framework/commit/64a0ff275c688b081942f9f416058f1124f51579)
- **[fix]** fix: fix — [8563ff12bc9e](https://github.com/laravel/framework/commit/8563ff12bc9e3ffe50ebe9aad49e2f29f13f5f16)
- **[change]** feat: redis cluster full support — [d6be8af77ce3](https://github.com/laravel/framework/commit/d6be8af77ce3bec95143e021cda2fff213f76e2c)

### Laravel 13.4.0 — 2026-04-07

- **[fix]** Fix `#[WithoutRelations]` queue attribute not being inherited by child classes — [#59568](https://github.com/laravel/framework/pull/59568)
- **[change]** Use #[Delay] attribute in Bus Dispatcher — [#59514](https://github.com/laravel/framework/pull/59514)
- **[change]** Use Delay attribute in NotificationSender — [#59513](https://github.com/laravel/framework/pull/59513)
- **[feature]** Add queue methods to inspect jobs — [#59511](https://github.com/laravel/framework/pull/59511)
- **[fix]** Fix missing `Illuminate\Queue\Attributes\Delay` attribute — [#59504](https://github.com/laravel/framework/pull/59504)

### Laravel 13.3.0 — 2026-04-01

- **[feature]** Add BatchStarted event — [#59458](https://github.com/laravel/framework/pull/59458)
- **[performance]** prefer `isset()` over `in_array()` for better performance — [#59457](https://github.com/laravel/framework/pull/59457)
- **[change]** Cache getLockForPopping() result in DatabaseQueue — [#59435](https://github.com/laravel/framework/pull/59435)
- **[feature]** Add enum support to QueueManager connection methods — [#59389](https://github.com/laravel/framework/pull/59389)
- **[change]** 13.x-show-mem-usage-in-verbose — [#59379](https://github.com/laravel/framework/pull/59379)
- **[fix]** Fix dependency injection of faked queueing dispatcher — [#59378](https://github.com/laravel/framework/pull/59378)
- **[change]** Update shared lock key documentation for clarity — [#59375](https://github.com/laravel/framework/pull/59375)
- **[feature]** Add lost connection to WorkerStopReason — [#59370](https://github.com/laravel/framework/pull/59370)
- **[change]** formatting — [628e7cb77ad2](https://github.com/laravel/framework/commit/628e7cb77ad29f1d67598a982e1045b339315824)

### Laravel 13.2.0 — 2026-03-24

- **[feature]** allow vaerdic for backoff — [#59354](https://github.com/laravel/framework/pull/59354)
- **[change]** 13.x-timedout-worker--stop-reason — [#59310](https://github.com/laravel/framework/pull/59310)
- **[feature]** Allow opting out of worker Job exception reporting — [#59308](https://github.com/laravel/framework/pull/59308)
- **[feature]** Allow for passing enums to attributes — [#59297](https://github.com/laravel/framework/pull/59297)
- **[change]** feat(queue): support enums in `#[Queue]` and `#[Connection]` — [#59278](https://github.com/laravel/framework/pull/59278)

### Laravel 13.1.1 — 2026-03-18

- **[change]** Break queue dependency — [#59275](https://github.com/laravel/framework/pull/59275)

### Laravel 13.1.0 — 2026-03-18

- **[change]** Use Carbon::now() and Date::now() instead of now() helper — [#59252](https://github.com/laravel/framework/pull/59252)
- **[fix]** Fix: Batch::add() wipes queue assignment on first job in array chains — [#59233](https://github.com/laravel/framework/pull/59233)

### Laravel 13.0.0 — 2026-03-17

- **[feature]** BatchCanceled carries the exception that cancelled the batch — [#59163](https://github.com/laravel/framework/pull/59163)
- **[change]** Update MonitorCommand.php — [#59151](https://github.com/laravel/framework/pull/59151)
- **[fix]** fix/queue-routes-docblocks — [#59119](https://github.com/laravel/framework/pull/59119)
- **[change]** adjust and tests — [#59118](https://github.com/laravel/framework/pull/59118)
- **[dx]** Add missing @throws into docblock for various methods — [#59016](https://github.com/laravel/framework/pull/59016)
- **[change]** Refactor parameter names that are implemented from the interface for consistency — [#59015](https://github.com/laravel/framework/pull/59015)
- **[breaking]** JobAttempted carries the exception, not a boolean — [#58954](https://github.com/laravel/framework/pull/58954)

  `JobAttempted::$exceptionOccurred`, a boolean, is replaced by `$exception`, which holds the `Throwable` or `null`. A listener checking the old property silently reads nothing.
  
  This is the event to count processed jobs from — it is the only one that distinguishes a job that was released by `RateLimited` or `WithoutOverlapping` from one that actually finished, via `isReleased()` and `successful()`. Now it can also say *why* without a second listener on `JobFailed`.

- **[breaking]** The Queue contract gained four size methods — [#58914](https://github.com/laravel/framework/pull/58914)

  `Illuminate\Contracts\Queue\Queue` now declares `pendingSize()`, `delayedSize()`, `reservedSize()` and `creationTimeOfOldestPendingJob()` alongside the `size()` it always had. Every first-party driver implements them; **any custom queue driver you maintain does not, and 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, and it is now something the framework will answer for any connection rather than something you compute by reading Redis yourself.

- **[change]** Queued notifications honour #[DeleteWhenMissingModels] — [#58908](https://github.com/laravel/framework/pull/58908)
- **[feature]** Job configuration as PHP attributes — [#58578](https://github.com/laravel/framework/pull/58578)

  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]` and `#[Delay]` following during the 13.x line, for twelve in 13.33.
  
  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](https://github.com/laravel/framework/pull/58477)
- **[dx]** Add missing chain method to interface — [#58429](https://github.com/laravel/framework/pull/58429)
- **[feature]** add dispatchAfterResponse to the Dispatcher Contract — [#58428](https://github.com/laravel/framework/pull/58428)
- **[feature]** Add starting to Monitor Contract — [#58399](https://github.com/laravel/framework/pull/58399)
- **[feature]** WorkerStopping says why the worker stopped — [#58341](https://github.com/laravel/framework/pull/58341)

  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](https://github.com/laravel/framework/pull/58094)

  `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 during an incident was a code change.
  
  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](https://github.com/laravel/framework/pull/56673)

  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](https://github.com/laravel/framework/pull/56148)
- **[feature]** Listeners can decide deleteWhenMissingModels at runtime — [#55508](https://github.com/laravel/framework/pull/55508)
- **[feature]** Add #[\Override] to the BatchFake class methods — [#55358](https://github.com/laravel/framework/pull/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), a full queue inspection API on the Queue contract, and debounceable jobs. Over the 13.x patch releases the largest single theme has been pausing: a global pause switch, pauses with a TTL, 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. 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.8, yes. pendingJobs(), delayedJobs() and reservedJobs() return the payloads as collections, allPendingJobs() and friends span every queue on the connection, and the counting methods — pendingSize(), delayedSize(), reservedSize(), totalSize() — answer the same questions without materialising anything. Before that, inspecting a backlog meant LRANGE against Redis and unserializing payloads by hand.

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