Queue & Jobs Guide
Last Updated: 2026-01-12 Status: Active Audience: Developers Plan Reference: 085-background-jobs-improvement.md
This guide documents queue and job patterns for the Client Portal application.
Table of Contents
- Overview
- Queue Monitoring Dashboard
- Job Metrics Tracking
- Creating Jobs
- Dispatching Jobs
- Job Configuration
- Error Handling
- Testing Jobs
- Queue Workers
Overview
Queues allow deferring time-consuming tasks:
- Sending emails
- Generating PDFs
- Processing uploads
- External API calls
- Data exports
Queue Drivers
| Driver | Use Case |
|---|---|
sync | Development (immediate execution) |
database | Production (simple setup) |
redis | Production (high performance) |
sqs | AWS deployments |
Queue Monitoring Dashboard
The admin panel includes a comprehensive queue monitoring dashboard at /admin/queues.
Accessing the Dashboard
Navigate to Admin → System → Queue Dashboard to access monitoring features.
Dashboard Features
| Feature | Description |
|---|---|
| Queue Status | Real-time sizes and health of all queues |
| Worker Status | Active workers, memory usage, uptime |
| Job Metrics | Processed, failed, avg duration today |
| Recent Jobs | List of recently processed jobs |
| Failed Jobs | List of failed jobs with retry/delete actions |
Routes
| Route | Method | Description |
|---|---|---|
admin.queues.index | GET | Main dashboard |
admin.queues.show | GET | Individual queue details |
admin.queues.analytics | GET | Analytics and trends |
admin.queues.priorities | GET | Queue priority configuration |
admin.queues.realtime | GET | Real-time metrics (AJAX) |
Queue Configuration
Configure monitored queues in config/queue.php:
'monitored_queues' => ['default', 'high', 'low', 'emails'],
Job Metrics Tracking
All job executions are automatically tracked via the JobEventSubscriber.
JobMetric Model
Location: app/Models/JobMetric.php
use App\Models\JobMetric;
// Query today's completed jobs
$completed = JobMetric::completed()->today()->count();
// Query failed jobs for a queue
$failed = JobMetric::failed()->forQueue('emails')->get();
// Get average duration
$avgDuration = JobMetric::completed()->today()->avg('duration_ms');
Recorded Data
| Field | Type | Description |
|---|---|---|
job_id | uuid | Unique job identifier |
queue | string | Queue name |
job_class | string | Full class name |
status | string | pending/processing/completed/failed |
queued_at | timestamp | When job was queued |
started_at | timestamp | When processing began |
completed_at | timestamp | When processing finished |
duration_ms | float | Execution time in milliseconds |
memory_mb | float | Memory used during execution |
exception | text | Error message if failed |
metadata | json | Additional job data |
Job Status Events
The JobStatusUpdated event broadcasts to the jobs channel:
Echo.channel('jobs')
.listen('.status.updated', (e) => {
console.log(e.short_name, e.status, e.duration_ms);
});
Scopes
// Filter by status
JobMetric::completed()->get();
JobMetric::failed()->get();
JobMetric::processing()->get();
// Filter by queue
JobMetric::forQueue('emails')->get();
// Filter by job class
JobMetric::forJobClass('App\\Jobs\\SendEmail')->get();
// Filter by time
JobMetric::today()->get();
JobMetric::since(now()->subHours(6))->get();
Creating Jobs
Generate a Job
php artisan make:job ProcessInvoicePdf
php artisan make:job SendInvoiceEmail
Basic Job Structure
<?php
namespace App\Jobs;
use App\Models\Invoice;
use App\Services\PdfService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessInvoicePdf implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
public Invoice $invoice
) {}
/**
* Execute the job.
*/
public function handle(PdfService $pdfService): void
{
$path = $pdfService->generateInvoicePdf($this->invoice);
$this->invoice->update(['pdf_path' => $path]);
}
}
Job with Dependencies
<?php
namespace App\Jobs;
use App\Models\Invoice;
use App\Mail\InvoiceMail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendInvoiceEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Invoice $invoice,
public ?string $customMessage = null
) {}
/**
* Execute the job.
*/
public function handle(): void
{
// Load relationships needed for email
$this->invoice->load(['client', 'items']);
Mail::to($this->invoice->client->email)
->send(new InvoiceMail($this->invoice, $this->customMessage));
$this->invoice->update(['emailed_at' => now()]);
}
}
Dispatching Jobs
Basic Dispatch
use App\Jobs\ProcessInvoicePdf;
// Dispatch to default queue
ProcessInvoicePdf::dispatch($invoice);
// Dispatch with arguments
SendInvoiceEmail::dispatch($invoice, 'Please review this invoice.');
Dispatch Options
// Delay execution
ProcessInvoicePdf::dispatch($invoice)
->delay(now()->addMinutes(5));
// Specific queue
ProcessInvoicePdf::dispatch($invoice)
->onQueue('pdfs');
// Specific connection
ProcessInvoicePdf::dispatch($invoice)
->onConnection('redis');
// Chain jobs
ProcessInvoicePdf::dispatch($invoice)
->chain([
new SendInvoiceEmail($invoice),
new LogInvoiceSent($invoice),
]);
Dispatch After Response
// Dispatch after HTTP response is sent
ProcessInvoicePdf::dispatchAfterResponse($invoice);
Conditional Dispatch
// Only dispatch if condition is true
ProcessInvoicePdf::dispatchIf($invoice->shouldGeneratePdf(), $invoice);
// Dispatch unless condition is true
ProcessInvoicePdf::dispatchUnless($invoice->hasPdf(), $invoice);
Synchronous Dispatch
// Execute immediately (bypass queue)
ProcessInvoicePdf::dispatchSync($invoice);
Job Configuration
Retry Configuration
class ProcessInvoicePdf implements ShouldQueue
{
/**
* Number of times to attempt the job.
*/
public int $tries = 3;
/**
* Number of seconds to wait before retrying.
*/
public int $backoff = 60;
/**
* Exponential backoff times.
*/
public array $backoff = [60, 120, 300];
/**
* Maximum exceptions before failing.
*/
public int $maxExceptions = 3;
}
Timeout Configuration
class ProcessLargeExport implements ShouldQueue
{
/**
* Job timeout in seconds.
*/
public int $timeout = 300; // 5 minutes
/**
* Prevent job from timing out during processing.
*/
public bool $failOnTimeout = true;
}
Unique Jobs
use Illuminate\Contracts\Queue\ShouldBeUnique;
class ProcessInvoicePdf implements ShouldQueue, ShouldBeUnique
{
/**
* Unique ID for preventing duplicates.
*/
public function uniqueId(): string
{
return $this->invoice->id;
}
/**
* Seconds the unique lock should be maintained.
*/
public int $uniqueFor = 3600;
}
Queue Priority
class SendInvoiceEmail implements ShouldQueue
{
/**
* Assign to high priority queue.
*/
public function __construct(public Invoice $invoice)
{
$this->onQueue('high');
}
}
// Or when dispatching
SendInvoiceEmail::dispatch($invoice)->onQueue('high');
Error Handling
Failed Job Handling
class ProcessInvoicePdf implements ShouldQueue
{
public function handle(PdfService $pdfService): void
{
$pdfService->generateInvoicePdf($this->invoice);
}
/**
* Handle job failure.
*/
public function failed(\Throwable $exception): void
{
// Log the failure
Log::error('Invoice PDF generation failed', [
'invoice_id' => $this->invoice->id,
'error' => $exception->getMessage(),
]);
// Notify admin
Notification::route('mail', config('client_portal.admin_email'))
->notify(new JobFailedNotification($this, $exception));
// Update invoice status
$this->invoice->update(['pdf_status' => 'failed']);
}
}
Retry Specific Exceptions
use App\Exceptions\TemporaryPdfException;
class ProcessInvoicePdf implements ShouldQueue
{
/**
* Exceptions that should trigger retry.
*/
public function retryUntil(): \DateTime
{
return now()->addHours(1);
}
/**
* Determine if exception should be reported.
*/
public function shouldRetry(\Throwable $e): bool
{
return $e instanceof TemporaryPdfException;
}
}
Manual Fail
public function handle(): void
{
if (!$this->invoice->client) {
$this->fail('Invoice has no associated client.');
return;
}
// Process...
}
Release Back to Queue
public function handle(): void
{
if ($this->someConditionNotMet()) {
// Put back on queue with delay
$this->release(60);
return;
}
// Process...
}
Testing Jobs
Test Job Dispatch
use App\Jobs\ProcessInvoicePdf;
use Illuminate\Support\Facades\Queue;
public function test_invoice_pdf_job_dispatched_on_send(): void
{
Queue::fake();
$invoice = Invoice::factory()->create();
$this->post(route('invoices.send', $invoice));
Queue::assertPushed(ProcessInvoicePdf::class, function ($job) use ($invoice) {
return $job->invoice->id === $invoice->id;
});
}
Test Job Execution
use App\Jobs\ProcessInvoicePdf;
use App\Services\PdfService;
public function test_invoice_pdf_is_generated(): void
{
$invoice = Invoice::factory()->create();
$pdfService = $this->mock(PdfService::class);
$pdfService->shouldReceive('generateInvoicePdf')
->once()
->with($invoice)
->andReturn('invoices/123.pdf');
$job = new ProcessInvoicePdf($invoice);
$job->handle($pdfService);
$this->assertEquals('invoices/123.pdf', $invoice->fresh()->pdf_path);
}
Test Job Chaining
public function test_jobs_are_chained(): void
{
Queue::fake();
$invoice = Invoice::factory()->create();
ProcessInvoicePdf::dispatch($invoice)->chain([
new SendInvoiceEmail($invoice),
]);
Queue::assertPushedWithChain(ProcessInvoicePdf::class, [
SendInvoiceEmail::class,
]);
}
Test Failed Jobs
public function test_failed_job_notifies_admin(): void
{
Notification::fake();
$invoice = Invoice::factory()->create();
$job = new ProcessInvoicePdf($invoice);
$exception = new \Exception('PDF generation failed');
$job->failed($exception);
Notification::assertSentTo(
Notification::route('mail', config('client_portal.admin_email')),
JobFailedNotification::class
);
}
Queue Workers
Running Workers
# Process all queues
php artisan queue:work
# Process specific queue
php artisan queue:work --queue=high,default
# Process single job then exit
php artisan queue:work --once
# Limit memory and time
php artisan queue:work --memory=128 --timeout=60
# Sleep between jobs
php artisan queue:work --sleep=3
Supervisor Configuration
; /etc/supervisor/conf.d/client-portal-worker.conf
[program:client-portal-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/client-portal/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/client-portal/storage/logs/worker.log
stopwaitsecs=3600
Restart Workers After Deploy
# Signal workers to restart after current job
php artisan queue:restart
Common Job Patterns
Batch Processing
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$invoices = Invoice::where('status', 'draft')->get();
$jobs = $invoices->map(fn ($invoice) => new SendInvoiceEmail($invoice));
$batch = Bus::batch($jobs)
->then(function (Batch $batch) {
// All jobs completed successfully
Log::info('Batch completed', ['id' => $batch->id]);
})
->catch(function (Batch $batch, \Throwable $e) {
// First failure detected
Log::error('Batch failed', ['error' => $e->getMessage()]);
})
->finally(function (Batch $batch) {
// Batch finished (success or failure)
})
->name('Send Invoices')
->dispatch();
Rate Limited Jobs
use Illuminate\Queue\Middleware\RateLimited;
class SendInvoiceEmail implements ShouldQueue
{
public function middleware(): array
{
return [new RateLimited('emails')];
}
}
// In AppServiceProvider
RateLimiter::for('emails', function ($job) {
return Limit::perMinute(30);
});
Preventing Overlapping Jobs
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProcessClientReport implements ShouldQueue
{
public function middleware(): array
{
return [
(new WithoutOverlapping($this->client->id))
->releaseAfter(60)
->expireAfter(300),
];
}
}
Best Practices
Do
- Keep jobs small and focused
- Use job chaining for sequential tasks
- Handle failures gracefully
- Set appropriate timeouts
- Use unique jobs to prevent duplicates
- Test job dispatch and execution
Don't
- Access request/session in jobs
- Perform heavy database queries in jobs without chunking
- Forget to handle exceptions
- Use sync driver in production
- Queue jobs for instant operations