Laravel 13.31 improves queue metrics and worker shutdowns
Two small changes for measuring full queue backlog and handling interrupted Laravel workers more clearly.
A single queue metric is rarely enough
Once an application splits work across several queues, looking at one queue at a time stops being very useful. Knowing the depth of emails does not answer the broader operational question: how much work is sitting in this queue connection overall? It is easy to end up maintaining custom sums for queue states or queue names just to answer that basic question.
Laravel 13.31 adds Queue::totalSize() to retrieve the overall size of the resolved queue connection. It is a small API, but it fits self-managed deployments where Redis or a database queue is part of the infrastructure you operate yourself. The release also introduces JobInterrupted, an event that identifies a job that was running when its worker received an interruption signal. (github.com)
A metric for the whole backlog
The method does not take a queue name. Laravel resolves the connection behind the Queue facade and returns an integer representing the total managed by that driver. With the Database driver, the implementation counts records in the jobs table rather than limiting the result to one named queue. (github.com)
For Redis, Laravel discovers queue names and adds their sizes together. For Laravel Cloud, the value is calculated from the queues managed by that connection. The important part is that totalSize() is an aggregate for the connection, not the depth of your highest-priority queue. (github.com)
A useful starting point for observability
I would treat it as one signal, not as a complete dashboard. A large backlog might mean too few workers, a slow dependency, or a temporary spike in work; the number alone cannot tell you which one applies. Still, collecting it regularly makes it easier to spot trends without maintaining separate queries for Database and Redis.
This can run from a scheduled command, an internal endpoint, or whatever process already ships metrics from the server. It does not assume a particular observability provider, which is useful when you run your own Docker hosts, VPS, or homelab. The key is to include the queue connection as a label so unrelated backlogs do not get mixed together.
<?php
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
$backlog = Queue::totalSize();
Log::info('queue.backlog', [
'connection' => config('queue.default'),
'jobs' => $backlog,
]);What the number actually includes
totalSize() is not limited to jobs ready to run. The tests added with the feature cover pending, delayed, and reserved jobs, so the total represents work held by the connection across those states. That makes sense for a capacity alert, because a reserved job still consumes worker capacity until it completes or becomes available again. (github.com)
If you need to understand whether workers are keeping up right now, pair it with per-queue size and the age of the oldest pending job. Laravel already exposes more targeted methods, including size(), pendingSize(), delayedSize(), and reservedSize(). The new method does not replace those narrower measurements. (api.laravel.com)
Interruption is not completion
The other change is about worker lifecycle. Laravel can receive signals such as SIGTERM, SIGQUIT, and SIGINT, then mark the worker to exit. When a job is currently running and implements Illuminate\Contracts\Queue\Interruptible, Laravel calls its interrupted(int $signal) method. (raw.githubusercontent.com)
After that method has run, the worker dispatches Illuminate\Queue\Events\JobInterrupted. The event exposes the connection name, the queue job object, and the received signal. That gives you a central place for logs, counters, or notifications without putting every observability concern inside each job class. (github.com)
Put cleanup logic in the job itself
If a job owns a temporary connection, supervises an external process, or holds a resource that should be released quickly, cleanup belongs in interrupted(). A JobInterrupted listener is valuable for recording what happened, but it receives the queue job wrapper rather than your already resolved command object. I would not make a global listener responsible for critical resource cleanup. (raw.githubusercontent.com)
A minimal implementation can look like this. The signal arrives as an integer, so you can record it or use it to update state consumed by cooperative long-running work. The method does not magically make a long job cancellable: the job code still needs to cooperate with interruption.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\Interruptible;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
final class GenerateReport implements ShouldQueue, Interruptible
{
use Queueable;
public function handle(): void
{
// Generate the report...
}
public function interrupted(int $signal): void
{
Log::warning('report.interrupted', [
'signal' => $signal,
]);
// Release resources or request a cooperative stop...
}
}Keep interruption logs outside business logic
The event is a good way to keep observability separate from business behavior. For example, you can write the queue job ID and connection name to your application log. That makes it much easier to correlate a container restart with the jobs that were active at the time.
<?php
use Illuminate\Queue\Events\JobInterrupted;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
Event::listen(JobInterrupted::class, function (JobInterrupted $event): void {
Log::warning('queue.job_interrupted', [
'connection' => $event->connectionName,
'job_id' => $event->job->getJobId(),
'signal' => $event->signal,
]);
});Docker is a practical use case, not a guarantee
Inside a container, this only works when the signal actually reaches the worker process. PHP must also have the pcntl extension available, because Laravel only registers asynchronous signal handlers when it can use that extension. I would verify both in the image before relying on this path for cleanup. (raw.githubusercontent.com)
The pattern is especially relevant for workers running through php artisan queue:work. These are long-lived processes, and Laravel recommends restarting workers during deployment so they load the latest code. JobInterrupted adds useful context when a restart overlaps with a job that is still working. (laravel.com)
The fine print around totalSize()
Not every driver can provide a useful global total. In the Laravel 13.31 change, Database, Redis, and Cloud calculate a total, while other driver implementations return zero. Before building a generic alert for every environment, test the behavior of the queue connection you actually use in production. (github.com)
It is not a transactional view of the whole system either. Jobs may be added or removed between reading the value and sending it to your metrics platform, so treat it as a point-in-time snapshot. Do not use it to decide that a business process has finished or as a replacement for explicit completion checks.
The fine print around JobInterrupted
JobInterrupted is not emitted for every worker shutdown. It does not fire when no job is running, or when the running job does not implement Interruptible. Laravel also requires the job to be running through its normal queued-job handler before it will pass the signal to it. (raw.githubusercontent.com)
It does not cover a forced termination, a process crash, or a timeout that kills the worker. This is a chance to react to the signals Laravel handles, not a recovery mechanism for every failure mode. For critical work, I would still rely on idempotent operations, persisted state, and deliberately designed retries.
Small changes that remove sticky code
Queue::totalSize() removes a repeated, driver-dependent sum when all you need is aggregate load. JobInterrupted gives you a precise way to distinguish an actually interrupted job from a worker that merely received a signal. Neither feature changes an application architecture, but both remove glue code in places where predictable behavior matters most: metrics and deployments.
- Source: Laravel News
- Official documentation: Laravel framework v13.31.0