A fundamental rule is simple: Don't make the user wait for something they don't need to wait for.
You get a request to register a domain. You validate it, store it in the database, and send a confirmation email. But the user doesn't care about the email being sent right now. They just want the domain registered.
Instead of making them wait 2 seconds for the email to send, send them an immediate response: "Domain registered! You'll receive a confirmation email shortly." Then send the email in the background.
That's what queues do. They let you quickly acknowledge the request, then handle the slow parts later.
Queued Jobs
Laravel queues let you dispatch work to background workers:
Setting Up Queues
# Generate queue configuration
php artisan queue:publish
# Set queue driver
# .env
QUEUE_CONNECTION=database
# or redis, sqs, etc.
Creating Queued Jobs
Process tasks in the background:
php artisan make:job SendToLogsJob
class SendToLogsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private int $maxExceptions = 3;
private int $backoff = 10; // seconds
public function __construct(
private string $method,
private string $path,
private int $status,
private array $payload,
) {
// Set priority (high, normal, low)
$this->onQueue('default');
}
public function handle(): void
{
try {
Log::channel('requests')->info('API Request', [
'method' => $this->method,
'path' => $this->path,
'status' => $this->status,
'payload' => $this->payload,
]);
} catch (Exception $e) {
$this->fail($e);
}
}
// Retry with exponential backoff
public function backoff(): array
{
return [10, 30, 60];
}
// Called when job fails after max attempts
public function failed(Exception $exception): void
{
Log::error('Failed to log request', [
'exception' => $exception::class,
'message' => $exception->getMessage(),
'job_data' => [
'method' => $this->method,
'path' => $this->path,
],
]);
}
}
Dispatching Jobs
Queue jobs from controllers or actions:
class LogApiRequestsMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Fire-and-forget (async)
SendToLogsJob::dispatch(
method: $request->getMethod(),
path: $request->getPathInfo(),
status: $response->status(),
payload: json_decode($response->getContent(), true),
);
return $response;
}
}
// Dispatch with constraints
SendToLogsJob::dispatch($data)
->onQueue('logging') // Route to specific queue
->delay(now()->addMinutes(5)); // Delay execution
->onConnection('redis'); // Use specific connection
Processing Jobs
Start queue workers to process jobs:
# Start queue worker (blocks, processes jobs as they arrive)
php artisan queue:work
# Process single job then exit (useful for cron)
php artisan queue:work --once
# Process specific queue
php artisan queue:work --queue=logging,default
# Monitor queue
php artisan queue:monitor
Job Priorities
Process important jobs first:
class HighPriorityJob implements ShouldQueue
{
public function __construct(private string $data)
{
$this->onQueue('high'); // High priority queue
}
}
class LowPriorityJob implements ShouldQueue
{
public function __construct(private string $data)
{
$this->onQueue('low');
}
}
// In worker config, process high before low
php artisan queue:work --queue=high,default,low
Handling Failures
Gracefully handle failed jobs:
class ReliableJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3; // Retry 3 times
public int $timeout = 30; // 30 second timeout
public int $maxExceptions = 1; // Max exceptions before failing
public function handle()
{
// If this throws after 3 tries, failed() is called
try {
$this->process();
} catch (Exception $e) {
if ($this->attempts() < $this->tries) {
$this->release($this->backoff()); // Retry later
} else {
$this->fail($e);
}
}
}
public function backoff(): int
{
// Exponential backoff: 10s, 20s, 40s
return 10 * (2 ** ($this->attempts() - 1));
}
public function failed(Exception $exception)
{
Notification::route('mail', 'admin@example.com')
->notify(new JobFailed($this, $exception));
}
private function process()
{
// Business logic
}
}
Async Request Logging
Log all requests asynchronously:
// config/logging.php
'channels' => [
'requests' => [
'driver' => 'single',
'path' => storage_path('logs/requests.log'),
'level' => 'info',
],
],
// Middleware that dispatches logging job
class LogApiRequestsMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Log asynchronously (fires immediately, processed by worker)
SendToLogsJob::dispatch(
method: $request->getMethod(),
path: $request->getPathInfo(),
status: $response->status(),
timestamp: now(),
);
return $response;
}
}