Step-by-step
-
1
Configure the Queue Driver
Set
QUEUE_CONNECTION=databasein your.envfile. Then generate the jobs table migration and run it. This creates thejobsandfailed_jobstables where your queue entries will live.bash# .env QUEUE_CONNECTION=database # Generate and run the migrations php artisan queue:table php artisan queue:failed-table php artisan migrate -
2
Create a Job Class
Generate a job with
make:job. The class gets ahandle()method where the work happens. Inject any dependencies directly — the service container resolves them when the worker picks up the job.bashphp artisan make:job SendWelcomeEmail -
3
Implement the Job
Implement
ShouldQueueto tell Laravel this job is queued (not synchronous). Store only what you need in the constructor — typically just the model or a small value object. Avoid storing large arrays or full request objects; they bloat the serialized job payload.php<?php namespace App\Jobs; use App\Mail\WelcomeMail; use App\Models\User; 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 SendWelcomeEmail implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 3; // Retry up to 3 times public int $backoff = 60; // Wait 60s between retries public function __construct( public readonly User $user ) {} public function handle(): void { Mail::to($this->user)->send(new WelcomeMail($this->user)); } public function failed(\Throwable $exception): void { // Notify the team, log, or clean up logger()->error('SendWelcomeEmail failed', [ 'user_id' => $this->user->id, 'error' => $exception->getMessage(), ]); } } -
4
Dispatch the Job
Dispatch from anywhere — a controller, a service, an event listener. The job is serialized, written to the
jobstable, and picked up by the worker asynchronously. Usedispatch()for immediate queuing ordispatch()->delay()to schedule it for later.phpuse App\Jobs\SendWelcomeEmail; // In a controller after registration SendWelcomeEmail::dispatch($user); // Delay by 5 minutes SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5)); // Send to a specific queue (useful for prioritisation) SendWelcomeEmail::dispatch($user)->onQueue('emails'); // Dispatch only if condition is met SendWelcomeEmail::dispatchIf($user->wants_email, $user); -
5
Run the Queue Worker
The worker process pulls jobs from the queue and runs them. Use
queue:workin development. It processes jobs continuously until you stop it. For production, use a process manager — Supervisor ensures the worker restarts automatically if it crashes.bash# Development php artisan queue:work # Process a specific queue with timeout and memory limit php artisan queue:work --queue=emails,default --timeout=60 --memory=128 # Process a single job then exit (useful in cron-based setups) php artisan queue:work --once -
6
Configure Supervisor for Production
Supervisor keeps the worker process running on the server. Create a config file in
/etc/supervisor/conf.d/. Thenumprocssetting controls how many worker processes run in parallel — scale it based on your job volume and server resources.ini[program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/esb1995.com/artisan queue:work database --sleep=3 --tries=3 --timeout=90 autostart=true autorestart=true stopasgroup=true killasgroup=true user=apache numprocs=2 redirect_stderr=true stdout_logfile=/var/log/worker.log stopwaitsecs=3600 -
7
Monitor and Retry Failed Jobs
When a job exceeds its
$trieslimit, it lands in thefailed_jobstable. Use the Artisan commands to inspect and retry. On production, consider setting up alerts when the failed jobs count grows — it means something upstream is broken.bash# List all failed jobs php artisan queue:failed # Retry a specific failed job by its UUID php artisan queue:retry 5 # Retry all failed jobs php artisan queue:retry all # Delete a specific failed job php artisan queue:forget 5 # Delete all failed jobs php artisan queue:flush -
8
Restart Workers After Deploy
Workers load your application code at startup. If you deploy new code without restarting workers, the running processes will keep using the old code. The safe pattern is to signal a restart after every deploy — the worker finishes its current job, then exits cleanly so Supervisor restarts it with the new code.
bash# Add this to your deploy script after git pull + optimize php artisan queue:restart # Supervisor will automatically start fresh workers with new code # (queue:restart signals via the cache — ensure your cache driver is shared)
Tips & gotchas
- Use named queues to prioritise work: <code>--queue=critical,high,default</code>. The worker drains <code>critical</code> completely before touching <code>high</code>. Put payment processing on <code>critical</code>, emails on <code>default</code>.
- Set <code>$timeout</code> on the job class to kill runaway jobs. Without it, a stuck job holds a worker slot forever.
- Avoid storing full Eloquent collections in a job constructor. Retrieve what you need inside <code>handle()</code> — the data may have changed between dispatch and execution anyway.
- <code>queue:work</code> loads your app once and keeps it in memory. Code changes are not picked up until the worker restarts. Always run <code>queue:restart</code> after deploy.
- For local development, set <code>QUEUE_CONNECTION=sync</code> in <code>.env</code> — jobs run immediately without needing a worker, making debugging much easier.
Wrapping up
Queues are not an optimisation — they are the correct architecture for any work that does not need to block a response. Get the database driver working locally, then switch to Redis in production for better throughput.