News

Queue::forward(): Reroute Laravel Queues in One Place

Published
Queue::forward(): Reroute Laravel Queues in One Place image

Queue names have a way of getting baked in everywhere. A job class sets onQueue('reports'), a dispatch site adds ->onConnection('redis'), a #[Queue] attribute pins another, and the worker fleet is configured to match. Then the reports queue starts starving everything else and you want it on its own Redis instance, or you move production to a managed queue service that wants FIFO naming, and the change touches every one of those places at once, plus whatever third-party packages dispatch to queues you do not control.

Laravel 13.26 adds Queue::forward(), contributed by @jackbayliss in #61188. It declares, in one place, that anything dispatched to a queue should land on a different queue, a different connection, or both. Job classes, dispatch sites, and vendor packages keep saying reports; the forward decides what that means today.

The API

All the shapes take a queue to match and a destination:

use Illuminate\Support\Facades\Queue;
 
// Rename the queue and move it to another connection
Queue::forward('reports', 'reports.fifo', 'cloud');
 
// Keep the name, move the connection
Queue::forward('payments', connection: 'cloud');
 
// Rename on the same connection
Queue::forward('updates', 'notifications');
 
// Several at once
Queue::forward([
'reports' => 'reports.fifo',
'emails' => 'emails.fifo',
], connection: 'cloud');

Queue names can be strings or backed enums, matching the enum support queues gained elsewhere. Calls belong in a service provider's boot() method, and they compose with the rest of the queue system because forwarding resolves through the same getConnection() hook that Queue::route() uses, with the rename applied by each driver's own queue resolution. There is no new contract for custom drivers to implement.

One subtlety in the matching: a forward that specifies a connection only rewrites the queue name for jobs that were headed to that connection. Queue::forward('reports', 'reports.fifo', 'cloud') renames reports on the cloud connection and moves un-targeted reports dispatches there, but a job explicitly dispatched to reports on your redis connection keeps its name. Forwards are a mapping, not a global find-and-replace.

Routing by Environment

The register-in-a-provider design makes the routing conditional in ways config files are bad at. The canonical case from the PR is an app that runs Redis locally and a managed queue service in production:

namespace App\Providers;
 
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
 
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
if ($this->app->isProduction()) {
Queue::forward([
'reports' => 'reports.fifo',
'emails' => 'emails.fifo',
], connection: 'cloud');
}
}
}

Locally, dispatch(new GenerateReport) goes to reports on Redis and queue:work picks it up as always. In production the identical code lands on reports.fifo on the managed connection. No environment checks in job classes, and no config/queue.php gymnastics to make one queue name mean two things.

The same shape covers the operational moves that used to require a deploy touching many files:

// The encoding queue is drowning the default Redis instance:
Queue::forward('encoding', connection: 'redis-heavy');
 
// Trial a new connection with one low-stakes queue before committing:
Queue::forward('notifications', connection: 'sqs-experiment');

Because the forward is one line, rolling back is deleting it. That makes gradual connection rollouts practical: move a queue, watch it, move the next.

What Forwarding Does Not Do

The forward applies at dispatch time, to jobs entering the queue from then on. Jobs already sitting on the old queue stay there, so a cutover needs workers draining the old name until it is empty. The PR is explicit about the steady state, though: running workers against both the original and forwarded names long-term invites race conditions, so once a forward is in place, treat the old name as deprecated and retire its workers after the drain.

A couple of adjacent notes. Forwarding rewrites destinations, it does not pause or throttle anything; for stopping consumption there is the queue pause API from Laravel 13.25. And if your listeners rather than jobs are the noisy part of the system, this release also shipped debounced queued listeners, which attack queue volume from the other end.

Further Reading

Paul Redmond photo

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

Filed in

Sponsored

laravelcloud logo
Laravel Cloud

Easily create and manage your servers and deploy your Laravel applications in seconds.

Visit Laravel Cloud

The latest

View all →
Laravel Tackle: Run an AI Coding Agent in Your Laravel App image

Laravel Tackle: Run an AI Coding Agent in Your Laravel App

Read article
Laravel Read-Through Filesystem: Lazy Storage Migration image

Laravel Read-Through Filesystem: Lazy Storage Migration

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

Read-Through Disks and Debounced Listeners in Laravel 13.26

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