romanzipp/laravel-queue-monitor
Laravel Queue Monitor tracks queue jobs in your database, recording status, duration, attempts, exceptions, and payloads. It adds a dashboard for searching, filtering, and inspecting failed or completed jobs, helping you debug and monitor queue performance without extra infrastructure.
Minimal setup (5.x):
composer require romanzipp/laravel-queue-monitor
php artisan vendor:publish --provider="romanzipp\QueueMonitor\Providers\QueueMonitorProvider" --tag=migrations
php artisan vendor:publish --provider="romanzipp\QueueMonitor\Providers\QueueMonitorProvider" --tag=config
php artisan migrate
php artisan queue:restart
Version 5.0 requires a new migration (queued_at column) even if you already ran the 4.x one. Publish the migrations tag again and migrate before restarting workers.
Then dispatch a test job and inspect the queue_monitor table or the package UI if enabled. With 5.x, a job should appear as soon as it is dispatched (queued), not only when a worker picks it up.
Where to look first:
config/queue-monitor.php — global monitoring toggle, queue allowlist, database connection/table, retention/pruning, UI settings, and the new monitor_queued_jobs flag.job_id, job_uuid, job_class, queue, status, attempt, retried, progress, queued_at, started_at, finished_at, exception, and data.romanzipp\QueueMonitor\Models\QueueMonitor — the main query surface with scopes/accessors for failed, processing, succeeded, recent, etc.romanzipp\QueueMonitor\Providers\Queue MonitorProvider) — confirms which Laravel queue events (JobQueued, JobProcessing, JobProcessed, JobFailed, etc.) are listened to and how records are created/updated.queue-monitor:* artisan commands.First practical use case: answer “what failed recently and why?” without digging through worker logs:
use romanzipp\QueueMonitor\Models\QueueMonitor;
QueueMonitor::failed()
->where('started_at', '>=', now()->subDay())
->latest()
->get();
Or use the bundled dashboard if enabled, typically mounted at /queue-monitor — verify the exact route and middleware in the package config/routes.
By default the package can monitor all queue jobs. That is useful for small/medium apps, but noisy for high-throughput systems. For day-to-day use:
IsMonitored trait when only critical jobs matter.5.0 adds a monitor_queued_jobs config option. When enabled, a monitor record is created the moment a job is dispatched, with queued_at populated. This is genuinely useful for spotting jobs that are stuck in the queue rather than failing:
// Jobs dispatched but never picked up (or waiting too long)
QueueMonitor::whereNotNull('queued_at')
->whereNull('started_at')
->where('queued_at', '<=', now()->subMinutes(30))
->get();
Practical implications:
queued_at to started_at on the same record. Both columns only exist reliably from 5.x onward.monitor_queued_jobs false and keep the previous behavior.Add the trait to jobs where you want friendly names, searchable metadata, or progress:
use Illuminate\Contracts\Queue\ShouldQueue;
use romanzipp\QueueMonitor\Traits\IsMonitored;
class ImportCustomers implements ShouldQueue
{
use IsMonitored;
public function monitorName(): ?string
{
return 'Import customers';
}
public function monitorData(): array
{
return [
'file_id' => $this->fileId,
'tenant_id' => $this->tenantId,
];
}
public function handle(): void
{
$this->monitorProgress(10);
// ...
$this->monitorProgress(100);
}
}
Keep monitorData() small and JSON-serializable. Store IDs, not Eloquent models or closures.
Use the model as an observability API rather than writing raw SQL:
QueueMonitor::processing()->get();
QueueMonitor::failed()->last24h()->count();
QueueMonitor::where('queue', 'emails')->succeeded()->count();
QueueMonitor::where('job_class', ImportCustomers::class)->latest()->first();
Use job_class_map in config to give internal class names readable labels in the dashboard/UI.
4.0 introduced job retry support with the job_uuid and retried columns; 5.0 builds on that. When reconstructing an incident, filter by job_uuid to trace a single logical job across its retries and original dispatch:
QueueMonitor::where('job_uuid', $uuid)->orderBy('attempt')->get();
This is the reliable way to link a queued record (5.x) to its later processing/failure records.
Schedule a lightweight check every few minutes:
$failed = QueueMonitor::failed()
->where('started_at', '>=', now()->subMinutes(10))
->count();
if ($failed > 0) {
// Slack / email / PagerDuty notification
}
Include the job class, queue, attempt, exception excerpt, and job_id in the alert. Avoid dumping the full exception or data payload into chat if it may contain PII.
With queued monitoring on, add a backlog check alongside failure alerting (see pattern above).
The monitor table grows with every monitored job, every progress update, and — with 5.0 — every dispatch. Schedule the package purge command, or prune via the model:
php artisan queue-monitor:purge
For high-volume queues, keep retention short — 7 to 30 days is usually enough for debugging. Longer retention belongs in a dedicated observability system.
5.0 adds PostgreSQL and SQLite support. This makes it viable for local SQLite-first development and Postgres production deployments. Configure the monitor connection/table explicitly if your app uses multiple databases.
If the bundled UI is enabled, protect it before exposing it outside local/staging:
auth or an admin gate via the package middleware/config.QueueMonitor from your own controller and reuse its scopes rather than duplicating status logic.queued_at column is added in 5.x. Run vendor:publish --tag=migrations and php artisan migrate before restarting workers, or inserts will fail.romanzipp\QueueMonitor\Providers\QueueMonitorProvider. If your publish commands or manual registrations reference the old service-provider path, update them during the 5.0 upgrade.monitor_queued_jobs is on by default in 5.x-style config. Read the published config and decide explicitly — it affects write volume and record counts across every dashboard and alert query you already have.php artisan queue:restart ensures long-running workers load the package listeners and new config.monitorProgress() at milestones, not on every loop iteration.job_uuid (4.x+) plus attempt to reconstruct what happened.failed_jobs table and queue backend still govern retries and failure handling.queued_at set and started_at null. Confirm in your version when a job first appears — especially for delayed jobs, sync drivers, and jobs that fail before handle().monitorData() must be serializable. Avoid models, resources, binary data, and large payloads. Store foreign keys and summaries instead.SELECT * on high-volume dashboards or alert queries.How can I help you explore Laravel packages today?