Laravel Best Practices: Writing Clean and Maintainable Code
Follow industry best practices for Laravel development.
In modern web applications, certain tasks like sending emails, processing images, or generating reports can take significant time to complete. If handled during web requests, they can lead to poor user experience and timeouts. Laravel Queues provide a powerful solution by allowing you to defer these time-consuming tasks to be processed in the background.
Why Use Queues?
Common Use Cases for Queues:
Laravel supports multiple queue drivers out of the box:
// config/queue.php
return [
'default' => env('QUEUE_CONNECTION', 'sync'),
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
],
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'failed_jobs',
],
];
# .env file
QUEUE_CONNECTION=database
REDIS_QUEUE=default
# Create jobs table migration
php artisan queue:table
# Create failed_jobs table
php artisan queue:failed-table
# Run migrations
php artisan migrate
php artisan make:job ProcessPodcast
This creates app/Jobs/ProcessPodcast.php:
<?php
// app/Jobs/ProcessPodcast.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
// Process the podcast...
}
}
<?php
// app/Jobs/SendWelcomeEmail.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $user;
public $tries = 3; // Number of retry attempts
public $timeout = 60; // Timeout in seconds
public $backoff = [10, 30, 60]; // Backoff delays between retries
/**
* Create a new job instance.
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Execute the job.
*/
public function handle(): void
{
// Send welcome email
Mail::to($this->user->email)
->send(new WelcomeEmail($this->user));
// Update user record
$this->user->update([
'welcome_email_sent_at' => now(),
]);
// Log the activity
logger("Welcome email sent to: {$this->user->email}");
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
// Notify admin of failure
logger()->error("Failed to send welcome email to: {$this->user->email}", [
'error' => $exception->getMessage(),
]);
// Mark user for manual follow-up
$this->user->update([
'needs_welcome_email' => true,
]);
}
}
<?php
// app/Http/Controllers/UserController.php
namespace App\Http\Controllers;
use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request)
{
// Validate and create user
$user = User::create($request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]));
// Dispatch welcome email job
SendWelcomeEmail::dispatch($user);
return response()->json([
'message' => 'User created successfully',
'user' => $user
], 201);
}
}
<?php
// Delay job execution by 10 minutes
SendWelcomeEmail::dispatch($user)
->delay(now()->addMinutes(10));
// Delay based on condition
SendWelcomeEmail::dispatch($user)
->delay(now()->addHours($user->timezone_offset));
<?php
// Dispatch to specific queue
SendWelcomeEmail::dispatch($user)
->onQueue('emails');
// Dispatch to specific connection and queue
SendWelcomeEmail::dispatch($user)
->onConnection('redis')
->onQueue('high-priority');
<?php
// Different ways to dispatch jobs
// Method 1: Static dispatch
SendWelcomeEmail::dispatch($user);
// Method 2: Dispatch helper
dispatch(new SendWelcomeEmail($user));
// Method 3: Through Bus facade
use Illuminate\Support\Facades\Bus;
Bus::dispatch(new SendWelcomeEmail($user));
// Method 4: Chain multiple jobs
Bus::chain([
new ProcessPodcast($podcast),
new OptimizePodcast($podcast),
new ReleasePodcast($podcast),
])->dispatch();
<?php
// app/Jobs/ProcessUploadedImage.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Image;
use Intervention\Image\Facades\Image as ImageProcessor;
use Illuminate\Support\Facades\Storage;
class ProcessUploadedImage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $image;
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*/
public function __construct(Image $image)
{
$this->image = $image;
$this->onQueue('image-processing');
}
/**
* Execute the job.
*/
public function handle(): void
{
$originalPath = Storage::disk('local')->path($this->image->file_path);
// Create different image sizes
$sizes = [
'thumbnail' => [150, 150],
'medium' => [400, 400],
'large' => [800, 800],
];
foreach ($sizes as $sizeName => $dimensions) {
$this->createImageSize($originalPath, $sizeName, $dimensions);
}
// Update image record
$this->image->update([
'processed_at' => now(),
'status' => 'processed',
]);
logger("Image {$this->image->id} processed successfully");
}
protected function createImageSize($originalPath, $sizeName, $dimensions)
{
[$width, $height] = $dimensions;
$image = ImageProcessor::make($originalPath);
// Resize maintaining aspect ratio
$image->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$filename = pathinfo($this->image->file_path, PATHINFO_FILENAME);
$extension = pathinfo($this->image->file_path, PATHINFO_EXTENSION);
$newPath = "images/{$sizeName}/{$filename}.{$extension}";
// Save processed image
Storage::disk('public')->put($newPath, $image->encode());
// Store size information
$this->image->sizes()->create([
'size_name' => $sizeName,
'file_path' => $newPath,
'width' => $image->width(),
'height' => $image->height(),
]);
}
/**
* Handle job failure.
*/
public function failed(\Throwable $exception): void
{
$this->image->update([
'status' => 'failed',
'error_message' => $exception->getMessage(),
]);
logger()->error("Failed to process image {$this->image->id}", [
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
}
}
<?php
// app/Jobs/SyncUserDataWithCRM.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\ConnectionException;
class SyncUserDataWithCRM implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $user;
public $tries = 5;
public $maxExceptions = 3;
public $backoff = [30, 60, 120, 300, 600]; // Exponential backoff
/**
* Create a new job instance.
*/
public function __construct(User $user)
{
$this->user = $user;
$this->onQueue('api-sync');
}
/**
* Execute the job.
*/
public function handle(): void
{
$response = Http::timeout(30)
->retry(3, 1000)
->withHeaders([
'Authorization' => 'Bearer ' . config('services.crm.api_key'),
'Content-Type' => 'application/json',
])
->post(config('services.crm.endpoint') . '/users', [
'email' => $this->user->email,
'name' => $this->user->name,
'phone' => $this->user->phone,
'company' => $this->user->company,
]);
if ($response->successful()) {
$crmData = $response->json();
$this->user->update([
'crm_id' => $crmData['id'],
'crm_synced_at' => now(),
]);
logger("User {$this->user->id} synced with CRM successfully");
} else {
throw new \Exception("CRM API returned error: " . $response->body());
}
}
/**
* Handle job failure.
*/
public function failed(\Throwable $exception): void
{
$this->user->update([
'crm_sync_failed' => true,
'last_sync_attempt' => now(),
]);
// Notify administrators
if ($this->attempts() >= $this->tries) {
$this->notifyAdmins($exception);
}
}
protected function notifyAdmins(\Throwable $exception): void
{
$admins = User::where('is_admin', true)->get();
foreach ($admins as $admin) {
// You could dispatch an email job here
logger()->error("CRM sync failed for user {$this->user->id} after {$this->attempts()} attempts", [
'admin_email' => $admin->email,
'error' => $exception->getMessage(),
'user_id' => $this->user->id,
]);
}
}
/**
* Determine the time at which the job should timeout.
*/
public function retryUntil(): \DateTime
{
return now()->addHours(2);
}
}
<?php
// app/Jobs/GenerateMonthlyReports.php
namespace App\Jobs;
use Illuminate\Bus\Batch;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use App\Models\Report;
use Illuminate\Support\Facades\Bus;
use Throwable;
class GenerateMonthlyReports implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $month;
public $year;
/**
* Create a new job instance.
*/
public function __construct($month = null, $year = null)
{
$this->month = $month ?? now()->subMonth()->month;
$this->year = $year ?? now()->subMonth()->year;
$this->onQueue('report-generation');
}
/**
* Execute the job.
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
return;
}
$users = User::where('active', true)->get();
$jobs = [];
foreach ($users as $user) {
$jobs[] = new GenerateUserReport($user, $this->month, $this->year);
}
Bus::batch($jobs)
->then(function (Batch $batch) {
logger("Monthly reports batch {$batch->id} completed successfully");
})
->catch(function (Batch $batch, Throwable $e) {
logger()->error("Monthly reports batch {$batch->id} failed: " . $e->getMessage());
})
->finally(function (Batch $batch) {
logger("Monthly reports batch {$batch->id} finished processing");
})
->name("Monthly Reports {$this->month}-{$this->year}")
->dispatch();
}
}
// Individual user report job
class GenerateUserReport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $user;
public $month;
public $year;
public function __construct(User $user, $month, $year)
{
$this->user = $user;
$this->month = $month;
$this->year = $year;
}
public function handle(): void
{
// Generate user-specific report
$reportData = $this->generateReportData();
$filename = "reports/user_{$this->user->id}_{$this->year}_{$this->month}.pdf";
// Generate PDF (using a hypothetical PDF service)
$pdfPath = PDF::generate($reportData, $filename);
// Store report record
Report::create([
'user_id' => $this->user->id,
'month' => $this->month,
'year' => $this->year,
'file_path' => $pdfPath,
'generated_at' => now(),
]);
// Notify user
dispatch(new SendReportNotification($this->user, $pdfPath));
}
protected function generateReportData(): array
{
// Generate report data for the user
return [
'user' => $this->user,
'month' => $this->month,
'year' => $this->year,
'statistics' => [
'login_count' => $this->getLoginCount(),
'orders_placed' => $this->getOrdersCount(),
'total_spent' => $this->getTotalSpent(),
],
];
}
protected function getLoginCount(): int
{
return $this->user->loginHistories()
->whereYear('login_at', $this->year)
->whereMonth('login_at', $this->month)
->count();
}
protected function getOrdersCount(): int
{
return $this->user->orders()
->whereYear('created_at', $this->year)
->whereMonth('created_at', $this->month)
->count();
}
protected function getTotalSpent(): float
{
return $this->user->orders()
->whereYear('created_at', $this->year)
->whereMonth('created_at', $this->month)
->sum('total_amount');
}
}
# Basic worker
php artisan queue:work
# Specific queue
php artisan queue:work --queue=emails
# Multiple queues with priorities
php artisan queue:work --queue=high,default,low
# With specific connection
php artisan queue:work --connection=redis
# With memory and timeout limits
php artisan queue:work --memory=128 --timeout=60
# For production (daemon mode)
php artisan queue:work --daemon --sleep=3 --tries=3
Create /etc/supervisor/conf.d/laravel-worker.conf:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work --sleep=3 --tries=3 --max-time=3600
directory=/home/forge/app.com
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
stopwaitsecs=3600
<?php
// app/Console/Commands/MonitorQueueHealth.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Http;
class MonitorQueueHealth extends Command
{
protected $signature = 'queue:monitor';
protected $description = 'Monitor queue health and metrics';
public function handle()
{
$metrics = $this->gatherMetrics();
$this->displayMetrics($metrics);
$this->checkThresholds($metrics);
}
protected function gatherMetrics(): array
{
return [
'pending_jobs' => $this->getPendingJobsCount(),
'failed_jobs' => $this->getFailedJobsCount(),
'worker_processes' => $this->getWorkerProcessCount(),
'queue_size' => $this->getQueueSizes(),
'oldest_job_age' => $this->getOldestJobAge(),
];
}
protected function getPendingJobsCount(): int
{
return DB::table('jobs')->count();
}
protected function getFailedJobsCount(): int
{
return DB::table('failed_jobs')->count();
}
protected function getWorkerProcessCount(): int
{
$output = shell_exec('ps aux | grep "queue:work" | grep -v grep | wc -l');
return (int) trim($output);
}
protected function getQueueSizes(): array
{
if (config('queue.default') === 'redis') {
return [
'default' => Redis::command('LLEN', ['queues:default']),
'emails' => Redis::command('LLEN', ['queues:emails']),
'high' => Redis::command('LLEN', ['queues:high']),
];
}
return [];
}
protected function getOldestJobAge(): ?int
{
$oldestJob = DB::table('jobs')
->orderBy('created_at', 'asc')
->first();
return $oldestJob ? now()->diffInMinutes($oldestJob->created_at) : null;
}
protected function displayMetrics(array $metrics): void
{
$this->info('Queue Health Metrics:');
$this->table(
['Metric', 'Value'],
[
['Pending Jobs', $metrics['pending_jobs']],
['Failed Jobs', $metrics['failed_jobs']],
['Worker Processes', $metrics['worker_processes']],
['Oldest Job Age (minutes)', $metrics['oldest_job_age'] ?? 'N/A'],
]
);
if (!empty($metrics['queue_size'])) {
$this->info('Queue Sizes:');
$queueTable = [];
foreach ($metrics['queue_size'] as $queue => $size) {
$queueTable[] = [$queue, $size];
}
$this->table(['Queue', 'Size'], $queueTable);
}
}
protected function checkThresholds(array $metrics): void
{
$alerts = [];
if ($metrics['pending_jobs'] > 1000) {
$alerts[] = "High pending jobs: {$metrics['pending_jobs']}";
}
if ($metrics['failed_jobs'] > 100) {
$alerts[] = "High failed jobs: {$metrics['failed_jobs']}";
}
if ($metrics['worker_processes'] < 2) {
$alerts[] = "Low worker count: {$metrics['worker_processes']}";
}
if ($metrics['oldest_job_age'] > 60) {
$alerts[] = "Old jobs in queue: {$metrics['oldest_job_age']} minutes";
}
if (!empty($alerts)) {
$this->alert('Queue Health Alerts:');
foreach ($alerts as $alert) {
$this->error($alert);
}
// Send notification (you could dispatch a notification job here)
logger()->warning('Queue health alerts triggered', $alerts);
}
}
}
<?php
// app/Jobs/Middleware/RateLimited.php
namespace App\Jobs\Middleware;
use Illuminate\Support\Facades\Redis;
class RateLimited
{
protected $key;
protected $allowed;
protected $every;
public function __construct($key = 'default', $allowed = 10, $every = 60)
{
$this->key = $key;
$this->allowed = $allowed;
$this->every = $every;
}
public function handle($job, $next)
{
Redis::throttle("job:{$this->key}")
->block(0)
->allow($this->allowed)
->every($this->every)
->then(function () use ($job, $next) {
$next($job);
}, function () use ($job) {
$job->release($this->every);
});
}
}
// Using the middleware in a job
class CallExternalAPI implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle()
{
// API call logic
}
public function middleware()
{
return [new RateLimited('external-api', 5, 60)];
}
}
<?php
// app/Http/Controllers/PodcastController.php
use Illuminate\Support\Facades\Bus;
use App\Jobs\ProcessPodcast;
use App\Jobs\OptimizePodcast;
use App\Jobs\ReleasePodcast;
use App\Jobs\NotifySubscribers;
public function publish(Podcast $podcast)
{
Bus::chain([
new ProcessPodcast($podcast),
new OptimizePodcast($podcast),
new ReleasePodcast($podcast),
function () use ($podcast) {
NotifySubscribers::dispatch($podcast);
// Update podcast status
$podcast->update(['published_at' => now()]);
},
])->catch(function ($exception) use ($podcast) {
// Handle any job failure in the chain
$podcast->update(['publish_status' => 'failed']);
logger()->error("Podcast publish chain failed: " . $exception->getMessage());
})->dispatch();
return response()->json(['message' => 'Podcast publishing started']);
}
<?php
// app/Events/OrderPlaced.php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\Order;
class OrderPlaced implements ShouldQueue
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
}
// app/Listeners/SendOrderConfirmation.php
namespace App\Listeners;
use App\Events\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendOrderConfirmation implements ShouldQueue
{
public function handle(OrderPlaced $event)
{
// Send order confirmation email
Mail::to($event->order->email)
->send(new OrderConfirmation($event->order));
}
}
// Usage in controller
event(new OrderPlaced($order));
<?php
// tests/Unit/Jobs/SendWelcomeEmailTest.php
namespace Tests\Unit\Jobs;
use Tests\TestCase;
use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Foundation\Testing\RefreshDatabase;
class SendWelcomeEmailTest extends TestCase
{
use RefreshDatabase;
public function test_welcome_email_is_sent()
{
Mail::fake();
$user = User::factory()->create();
// Dispatch the job
SendWelcomeEmail::dispatch($user);
// Assert email was sent
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}
public function test_user_is_updated_after_email_sent()
{
$user = User::factory()->create();
SendWelcomeEmail::dispatchSync($user); // Sync for testing
$this->assertNotNull($user->fresh()->welcome_email_sent_at);
}
public function test_job_is_pushed_to_queue()
{
Queue::fake();
$user = User::factory()->create();
// Dispatch job
SendWelcomeEmail::dispatch($user);
// Assert job was pushed to queue
Queue::assertPushed(SendWelcomeEmail::class, function ($job) use ($user) {
return $job->user->id === $user->id;
});
}
public function test_job_failure_handling()
{
$user = User::factory()->create();
// Mock a failure
Mail::shouldReceive('to->send')
->andThrow(new \Exception('SMTP error'));
$job = new SendWelcomeEmail($user);
$job->failed(new \Exception('SMTP error'));
$this->assertTrue($user->fresh()->needs_welcome_email);
}
}
<?php
// tests/Unit/Jobs/GenerateMonthlyReportsTest.php
namespace Tests\Unit\Jobs;
use Tests\TestCase;
use App\Jobs\GenerateMonthlyReports;
use App\Jobs\GenerateUserReport;
use App\Models\User;
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
use Illuminate\Foundation\Testing\RefreshDatabase;
class GenerateMonthlyReportsTest extends TestCase
{
use RefreshDatabase;
public function test_batch_is_dispatched()
{
Bus::fake();
User::factory()->count(5)->create(['active' => true]);
// Dispatch the batch job
GenerateMonthlyReports::dispatch();
// Assert batch was dispatched
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->jobs->count() === 5 &&
$batch->name === 'Monthly Reports';
});
}
public function test_only_active_users_get_reports()
{
Bus::fake();
$activeUsers = User::factory()->count(3)->create(['active' => true]);
User::factory()->count(2)->create(['active' => false]);
GenerateMonthlyReports::dispatch();
Bus::assertBatched(function (PendingBatch $batch) use ($activeUsers) {
return $batch->jobs->count() === 3;
});
}
}
// config/queue.php for high-throughput applications
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => 5, // Wait up to 5 seconds for jobs
'after_commit' => false,
],
# Monitor queue performance
php artisan queue:monitor
# Scale workers based on queue size
#!/bin/bash
# scale-workers.sh
QUEUE_SIZE=$(php artisan queue:size --queue=emails)
WORKER_COUNT=$(($QUEUE_SIZE / 100 + 1))
# Ensure minimum of 1, maximum of 10 workers
WORKER_COUNT=$((WORKER_COUNT < 1 ? 1 : WORKER_COUNT))
WORKER_COUNT=$((WORKER_COUNT > 10 ? 10 : WORKER_COUNT))
echo "Scaling to $WORKER_COUNT workers for queue size $QUEUE_SIZE"
# Update supervisor configuration
sed -i "s/numprocs=.*/numprocs=$WORKER_COUNT/" /etc/supervisor/conf.d/laravel-worker.conf
supervisorctl update
# Restart workers
php artisan queue:restart
# Clear stuck jobs
php artisan queue:flush
# Retry failed jobs
php artisan queue:retry all
# Use with memory limit
php artisan queue:work --memory=128
# Process limited number of jobs
php artisan queue:work --max-jobs=100
// Use database indexing for jobs table
Schema::table('jobs', function (Blueprint $table) {
$table->index(['queue', 'reserved_at']);
$table->index(['queue', 'available_at']);
});
You've now mastered Laravel Queues! From basic job creation to advanced patterns like batching and middleware, you have the tools to build robust, scalable background processing for your Laravel applications.