News

Read-Through Disks and Debounced Listeners in Laravel 13.26

Published
Read-Through Disks and Debounced Listeners in Laravel 13.26 image

Laravel 13.26 adds a read-through filesystem driver that layers a primary disk over a fallback and promotes files on first read, debouncing for queued event listeners, and a Queue::forward() method that reroutes queues without touching job classes. The Laravel team released v13.26.0 on August 18, 2026.

  • A read-through filesystem driver with an optional no-copy mode
  • #[DebounceFor] now works on queued event listeners
  • Queue::forward() reroutes a queue to another queue and/or connection
  • Iterable process pools, a dedicated idle timeout exception, and new Process fake assertions
  • orWhereKey(), closure support in wherePivot(), and enums in inOrderOf()
  • A JobReleased event and paused-queue notices in worker output
  • Guzzle 8 support and a batch of Redis cluster fixes

What's New

Read-Through Filesystem Disks

A new read-through driver combines two existing disks into one. Reads check the primary disk first and fall back to the second, copying the file to the primary on the way through, so the primary fills up lazily with the files that are actually requested. Writes, deletes, and directory listings go to the primary only:

'assets' => [
'driver' => 'read-through',
'primary' => 'r2',
'fallback' => 'legacy-s3',
],

The disks can be named like above or defined inline as config arrays, and a copy => false option serves from the fallback without promoting anything, which fits development environments pointed at production files. Promotion failures are swallowed by default so a read never breaks because the copy did; set throw_on_promotion_failure => true to surface them.

Contributed by @taylorotwell in #61140, with the no-copy option by @jimbojsb in #61155. We cover the promotion mechanics, the caveats, and a full bucket-migration workflow in our read-through filesystem deep dive.

Debounced Queued Listeners

The #[DebounceFor] attribute that debounced queued jobs in Laravel 13.6 now works on queued event listeners. When the same event fires repeatedly, only the last dispatch inside the window runs the listener:

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\DebounceFor;
 
#[DebounceFor(30, maxWait: 120)]
class UpdateProductSearchIndex implements ShouldQueue
{
public function debounceId(ProductUpdated $event): string
{
return (string) $event->product->getKey();
}
 
public function handle(ProductUpdated $event): void
{
// reindex once, with the latest state
}
}

debounceId() scopes the window per resource, maxWait caps how long a busy stream can keep deferring the work, and the debounce applies to this listener alone, so other listeners on the same event still run every time. A debounced listener cannot also implement ShouldBeUnique; the dispatcher throws a LogicException, since uniqueness keeps the first dispatch and debouncing keeps the last.

Contributed by @stevebauman in #61169.

Queue::forward()

Queue::forward() reroutes everything dispatched to a queue onto another queue, another connection, or both, from one place in a service provider and with no edits to job classes or dispatch sites:

use Illuminate\Support\Facades\Queue;
 
Queue::forward('reports', 'reports.fifo', 'cloud'); // rename and move connection
Queue::forward('payments', connection: 'cloud'); // keep the name, move connection
Queue::forward('updates', 'notifications'); // rename on the same connection
 
Queue::forward([
'reports' => 'reports.fifo',
'emails' => 'emails.fifo',
], connection: 'cloud');

Forwards resolve through the same hook as Queue::route(), so they apply however the job reaches the queue. Contributed by @jackbayliss in #61188.

Process Improvements

Three additions land around the Process facade, all from @xurshudyan. Process pools and their results are now iterable (#61184). Previously, foreach over ProcessPoolResults iterated public properties, of which there are none, so a loop written to check exit codes silently checked nothing. Both classes now implement IteratorAggregate.

An idle timeout now throws its own ProcessIdleTimedOutException (#61182). The two timeouts mean opposite things, a hung process versus a slow one, but both previously threw ProcessTimedOutException:

try {
Process::timeout(600)->idleTimeout(30)->run('./deploy.sh');
} catch (ProcessIdleTimedOutException $e) {
// no output for 30 seconds: kill and alert
} catch (ProcessTimedOutException $e) {
// still producing output, just slow: retry with more time
}

And the Process fake gained assertion helpers (#61193, #61197): assertRanCount(2) checks how many processes ran, assertRanInOrder([...]) checks the sequence, and recorded() filters the recorded process and result pairs with a callback. The order and count assertions accept array commands as well as strings.

Eloquent Builder Additions

orWhereKey() and orWhereKeyNot() join the existing whereKey() pair, replacing the orWhereIn('id', ...) fallback in queries that needed a primary key condition in an or branch (@calebdw, #61154).

wherePivot() and orWherePivot() accept a closure that receives a builder scoped to the pivot model, so scopes defined on a custom pivot class are callable from the relationship query (@calebdw, #61150):

$project->subscribers()
->wherePivot(fn ($query) => $query->active()->notMuted())
->get();

And inOrderOf() accepts enums in its value list, matching the enum support elsewhere in the query builder (@ziadoz, #61147):

$orders = Order::query()
->inOrderOf('status', [Status::Pending, Status::Processing, Status::Shipped])
->get();

Relations also gained a getRelatedClass() accessor (#61222).

Queue Worker Visibility

A new JobReleased event fires when a job is released back onto the queue from its own handler or from middleware such as WithoutOverlapping (#61108). The existing JobReleasedAfterException only covered releases caused by a throw, so an overlap-triggered release was invisible.

Workers also now print a notice when a queue they consume is paused, and again when it resumes (#61142). Since the pause API arrived, a paused worker just stopped producing output, which read as a hang. A worker started against an already-paused queue reports that too. Both contributed by @jackbayliss.

Other Fixes and Improvements

  • Guzzle 8 is now supported alongside Guzzle 7 (#60321)
  • db:seed --class=SpecificSeeder shows the same RUNNING/DONE progress output that nested seeders already got through Seeder::call() (#61186)
  • A run of Redis hardening: paused-queue checks no longer perform cross-slot reads on clusters (#61139), cache tag pruning scans every master node (#61174) and no longer loops forever on stale tags (#61181), a failed pipeline or transaction leaves the connection usable (#61183), the phpredis client is rebuilt after a cluster response error (#61214), and some commands retry on transient failures (#61175)
  • The Cloud queue driver exposes managedQueues() (#61149), managed queue events promote nested data to the top level (#61209), and timed-out cloud agent long-polls are retried (#61177)
  • throwUnless() no longer silently does nothing in a case where it should throw (#61217), and Request input key order is preserved when merging files (#61221)
  • The readonly=false setting is respected in Microsoft SQL Server configuration (#61141), single quotes are escaped in Postgres JSON path attributes (#61192), and array JSON:API query parameters no longer cause a TypeError (#61206)
  • The pail dev command is only registered when Pail is installed (#61157), artisan dev stops repeatedly asking to install @laravel/multiplex (#61167), and the pinned multiplex version was removed (#61171)
  • Test-facing touches: session errors keep the original assertion failure message (#61201), a setTestNow() reuse issue with now() was fixed (#61190), and the expectsQuestion() docblock allows array answers (#61223)

Upgrade Notes

No breaking changes are expected for typical applications. Two edges worth knowing: a listener carrying #[DebounceFor] now throws a LogicException if it also implements ShouldBeUnique, and the whereKey() / whereKeyNot() internals were consolidated in #61154, which only matters if you extend the Eloquent builder and override those methods. Review the changelog for PR-by-PR details when upgrading.

References

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Sponsored

serpapi logo
SerpApi

The Web Search API for Your LLM and AI Applications

Visit SerpApi

The latest

View all →
Laravel Read-Through Filesystem: Lazy Storage Migration image

Laravel Read-Through Filesystem: Lazy Storage Migration

Read article
Lerd: A Free, Open Source Herd Alternative for Linux and macOS image

Lerd: A Free, Open Source Herd Alternative for Linux and macOS

Read article
Let's Encrypt HTTPS on an IP Address With FrankenPHP image

Let's Encrypt HTTPS on an IP Address With FrankenPHP

Read article
Laravel Chores: Resumable Data Operations and Cleanups image

Laravel Chores: Resumable Data Operations and Cleanups

Read article
NativePHP v4: Build Native iOS and Android UI in Blade image

NativePHP v4: Build Native iOS and Android UI in Blade

Read article
Laravel Lock: Distributed Locks for Models and Routes image

Laravel Lock: Distributed Locks for Models and Routes

Read article