Clean Up Your Jobs Config With Laravel 13's Queue::route()

5 min read

In a large Laravel application, queue configuration tends to spread everywhere. One job has public string $queue = 'high'; as a property. Another has the queue set in the constructor. A third gets its queue name injected at a dozen dispatch sites with ->onQueue('emails'). When you need to rename a queue or move to a different Redis connection, you're doing a find-and-replace across the whole codebase.

Laravel 13 adds Queue::route() to fix this. One method, in one service provider, declares the routing for every job class.

If you're still on Laravel 12, check the practical guide to upgrading from Laravel 12 to Laravel 13 before diving in.

How Laravel 13 Queue::route() Works

Queue::route() registers a mapping between a job class and its default queue name and connection. The dispatcher checks this map before falling back to the connection's configured default.

The full resolution order when a job is dispatched is:

  1. A dispatch-time override (->onQueue() / ->onConnection())
  2. A PHP attribute on the class (#[Queue] / #[Connection])
  3. A registered Queue::route() entry
  4. The connection's default queue

Queue::route() sits at position three. It's a centralised default, not a lock. You can still override it per-dispatch when you need to.

Registering Routes in a Service Provider

Call Queue::route() inside AppServiceProvider::boot(). Pass the job class name, then the queue and connection as named parameters:

use Illuminate\Support\Facades\Queue;
use App\Jobs\ProcessPayment;
use App\Jobs\GenerateReport;
use App\Jobs\SendNotification;

public function boot(): void
{
    // Route a single job to a specific queue and connection
    Queue::route(ProcessPayment::class, queue: 'high', connection: 'redis-priority');

    // Route a job to just a queue (uses the default connection)
    Queue::route(GenerateReport::class, queue: 'low');
}

For a large application, routing a dozen jobs inline can clutter boot(). The array syntax keeps it tidy — but pay attention to the order: connection comes first, queue second:

public function boot(): void
{
    Queue::route([
        ProcessPayment::class   => ['redis-priority', 'high'],
        GenerateReport::class   => ['redis', 'low'],
        SendNotification::class => ['redis', 'emails'],
    ]);
}

I prefer the named-parameter form for one or two jobs and the array form once you're routing five or more.

A Real-World Example

Before Queue::route(), a typical payment job looked like this:

class ProcessPayment implements ShouldQueue
{
    // Queue config lives in the job class — mixed with business logic
    public string $connection = 'redis-priority';
    public string $queue = 'high';
    public int $tries = 3;
    public int $timeout = 30;

    public function handle(): void
    {
        // ...
    }
}

And the dispatch site:

// Redundant — queue already declared on the class
ProcessPayment::dispatch($order)->onQueue('high')->onConnection('redis-priority');

After Queue::route(), the job is clean:

class ProcessPayment implements ShouldQueue
{
    public int $tries = 3;
    public int $timeout = 30;

    public function handle(): void
    {
        // ...
    }
}

And the service provider holds all routing:

public function boot(): void
{
    Queue::route([
        // Payment jobs: high-priority Redis instance
        ProcessPayment::class   => ['redis-priority', 'high'],
        RefundPayment::class    => ['redis-priority', 'high'],

        // Reporting jobs: standard Redis, low-priority queue
        GenerateReport::class   => ['redis', 'low'],
        ExportCsvReport::class  => ['redis', 'low'],

        // Notifications: standard Redis, dedicated emails queue
        SendWelcomeEmail::class => ['redis', 'emails'],
        SendInvoiceEmail::class => ['redis', 'emails'],
    ]);
}

Now when the infrastructure team migrates payment processing to a dedicated Redis cluster, one line changes in one file. Nothing in the job classes, nothing at the dispatch sites.

For more on designing queue topologies for production — including when to split connections and how to size workers — see scaling Laravel queues in production.

Overriding at Dispatch Time

The route is a default, not a constraint. A dispatch-time override takes the highest precedence:

// Normally routed to 'high' on 'redis-priority' by Queue::route()
// This specific dispatch goes to 'urgent' instead
ProcessPayment::dispatch($order)->onQueue('urgent');

Under the hood, getAttributeValue() checks whether the job's queue property was explicitly set (i.e., differs from the class default) before checking the #[Queue] attribute or route. So ->onQueue() genuinely wins — it's not just documented behaviour, it's enforced by the resolution logic.

Routing by Interface or Parent Class

The route resolver checks not just the exact class, but also parent classes, interfaces, and traits. This means you can route an entire group of jobs by interface:

interface HighPriorityJob {}

class ProcessPayment implements ShouldQueue, HighPriorityJob { ... }
class RefundPayment implements ShouldQueue, HighPriorityJob { ... }
// Routes all HighPriorityJob implementors in one line
Queue::route(HighPriorityJob::class, queue: 'high', connection: 'redis-priority');

This is useful when a new job class is added — if it implements the right interface, it automatically inherits the correct routing without touching the service provider.

Gotchas and Edge Cases

Array format is [connection, queue] — not [queue, connection]. When using the array syntax, the values are [connection, queue]. Getting this backwards will route jobs to the wrong connection silently.

A null connection uses the default. Queue::route(MyJob::class, queue: 'high') sets the queue but leaves the connection as null, which resolves to whatever QUEUE_CONNECTION is set to in your environment. That's usually what you want — just be explicit in production configs.

Routes are not validated at registration time. If you typo a queue name or reference a connection that doesn't exist in config/queue.php, you won't see an error until a job is dispatched. Keep your queue.php connections and your route registrations in sync.

PHP attributes still take precedence over routes. If a job class has a #[Queue('override')] attribute, it overrides the route for every dispatch (unless a dispatch-time ->onQueue() is used). I prefer to pick one approach per project: either attributes or routes, not both. Routes are better for infrastructure decisions; attributes are better for domain decisions.

For long-running workers, pair this with the advice in stop queue workers from leaking memory with --max-jobs and --max-time — centralized routing makes it easier to assign specific worker pools to specific queues.

Wrapping Up

Queue::route() is a small addition with a meaningful quality-of-life payoff. Register it once in your service provider, keep your job classes focused on business logic, and rename queues or swap connections without touching anything outside of one file.

Once your routing is in place, the natural next step is observing it — monitoring your Laravel queues with Horizon in production walks through setting up metrics and alerts so you know when a queue is backing up or a worker is starved.

laravel
queues
laravel-13
Steven Richardson

Steven is a software engineer with a passion for building scalable web applications. He enjoys sharing his knowledge through articles and tutorials.