A queue:work process can exit for nine different reasons, and until Laravel 13.30 the console output looked the same for all of them. A worker running under Supervisor that restarts every few minutes might be hitting its memory limit, losing its database connection, or picking up a queue:restart from a deploy. Telling those apart meant registering an event listener or lining the restart up against a deploy log.
The command now writes the reason on its last line:
2026-09-01 13:20:40 Worker STOPPED Memory limit exceeded
Console and JSON Output
WorkCommand registers a listener for WorkerStopping next to the ones it already had for job and queue status, so the stop line comes out in the same two formats as the rest of the output. With --json, it is a record alongside the per-job lines:
{"level":"info","status":"stopped","reason":"empty","exit_code":0,"jobs_processed":12,"memory":34.0,"timestamp":"2026-09-01T13:20:40.118273+00:00"}
reason is the enum's backing value, which is stable to match on. memory is megabytes rounded to one decimal, or null when the worker recorded no figure. level is info for exit code 0 and warning for anything else.
The line is skipped under --quiet and --silent, and when the reason is null. A worker killed from outside, by an OOM killer or a SIGKILL from somewhere else, never runs the code that dispatches the event, so those exits stay silent.
Stop Reasons and Exit Codes
WorkerStopReason is an existing enum. This change added a description() method to it, so the console output and any listener you write read the same strings:
| Value | Description | Exit code |
|---|---|---|
empty |
Queue empty | 0 |
empty_for |
Queue empty for the configured duration | 0 |
max_jobs |
Maximum jobs exceeded | 0 |
max_time |
Maximum run time exceeded | 0 |
restart_signal |
Received restart signal | 0 |
interrupted |
Interrupted | 0 |
lost_connection |
Lost connection | 0 |
memory |
Memory limit exceeded | 12 |
timed_out |
Job timed out | 1 |
Filtering the JSON Output
Once the reason is a field, finding it in a log is one filter:
php artisan queue:work --json --max-time=3600 2>&1 | \ jq -c 'select(.status == "stopped")'
Under a process manager writing to a file, the same query runs against the log after the fact.
The WorkerStopping Event
The event is still the place to go for anything beyond reading output. It carries the reason along with the exit status, the number of jobs processed, the timestamp of the last job, and the memory in use when the worker quit:
use Illuminate\Queue\Events\WorkerStopping; Event::listen(function (WorkerStopping $event) { Log::info('Worker stopped', [ 'reason' => $event->reason?->value, 'status' => $event->status, 'jobs' => $event->jobsProcessed, 'memory' => $event->memoryUsage, ]);});
Use it to count exits per reason for a dashboard, or to push each one into a metrics backend.
Contributed by Jack Bayliss in #61339.