Web applications often need to perform tasks that take several seconds to complete: sending emails, resizing images, calling external APIs, or generating reports. Running these tasks synchronously during an HTTP request means users wait until the work finishes.Laravel’s queue system lets you push these tasks to a background queue and return a response immediately. A worker process picks up the jobs and executes them separately.
Laravel supports multiple queue backends—database, Redis, Amazon SQS, and more. During development, the sync driver executes jobs immediately without a real queue, so no worker is required.
Amazon SQS limits the maximum queued message payload size. If your jobs may exceed that limit, you can store oversized payloads in a cache store and send only a pointer through SQS:
<?phpnamespace App\Jobs;use App\Models\User;use App\Mail\WelcomeMail;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Queue\Queueable;use Illuminate\Support\Facades\Mail;class SendWelcomeEmail implements ShouldQueue{ use Queueable; public function __construct( public User $user, ) {} public function handle(): void { Mail::to($this->user->email)->send(new WelcomeMail($this->user)); }}
Implementing ShouldQueue tells Laravel to push the job onto the queue instead of running it synchronously. The Queueable trait provides the methods needed to configure and dispatch the job.
When you pass an Eloquent model to a job constructor, Laravel serializes only the model’s ID. The worker re-fetches fresh data from the database at execution time, keeping the queue payload small.
Instead of calling onQueue() and onConnection() on every job dispatch, use the Queue facade’s route method in a service provider’s boot() to define default routing rules for specific job classes:
use App\Concerns\RequiresVideo;use App\Jobs\ProcessPodcast;use App\Jobs\ProcessVideo;use Illuminate\Support\Facades\Queue;public function boot(): void{ Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts'); Queue::route(RequiresVideo::class, queue: 'video');}
You can route by interface, trait, or parent class — any job that implements, uses, or extends it will inherit the rule.To route multiple job classes at once, pass an array:
Queue::route([ ProcessPodcast::class => ['podcasts', 'redis'], // queue and connection ProcessVideo::class => 'videos', // queue only (default connection)]);
Queue routing can still be overridden per-dispatch with onQueue() or onConnection().
When you need to dispatch many independent jobs at once without batch tracking or callbacks, use the bulk method on the Bus facade. Laravel groups the jobs by their configured queue connection and queue name, then pushes each group to the appropriate queue in bulk:
use App\Jobs\ProcessUser;use Illuminate\Support\Facades\Bus;Bus::bulk( $users->map(fn ($user) => new ProcessUser($user)));
Bus::bulk() sends jobs to the queue in bulk groups. Unlike Bus::batch(), it doesn’t provide progress tracking or completion callbacks. Use it when you want to efficiently dispatch a large number of simple, independent jobs in one call.
Chain jobs so they run sequentially. If one job in the chain fails, subsequent jobs are not run:
use App\Jobs\ProcessPodcast;use App\Jobs\OptimizePodcast;use App\Jobs\ReleasePodcast;use Illuminate\Support\Facades\Bus;Bus::chain([ new ProcessPodcast($podcast), new OptimizePodcast($podcast), new ReleasePodcast($podcast),])->dispatch();
You can also attach a callback that runs when the entire chain completes:
Bus::chain([ new ProcessPodcast($podcast), new ReleasePodcast($podcast),])->catch(function (Throwable $e) { // A job in the chain failed...})->dispatch();
Batching lets you dispatch a collection of jobs and track their collective progress. Start by creating a migration for the job_batches table:
php artisan make:batches-tablephp artisan migrate
Implement the Batchable trait in your job class:
<?phpnamespace App\Jobs;use Illuminate\Bus\Batchable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Queue\Queueable;class ImportContacts implements ShouldQueue{ use Batchable, Queueable; public function handle(): void { if ($this->batch()->cancelled()) { return; } // Import a chunk of contacts... }}
Dispatch a batch using Bus::batch:
use App\Jobs\ImportContacts;use Illuminate\Bus\Batch;use Illuminate\Support\Facades\Bus;use Throwable;$batch = Bus::batch([ new ImportContacts($chunkA), new ImportContacts($chunkB), new ImportContacts($chunkC),])->then(function (Batch $batch) { // All jobs completed successfully})->catch(function (Batch $batch, Throwable $e) { // A job failed})->finally(function (Batch $batch) { // The batch has finished executing})->dispatch();
Inspect a batch by its ID:
$batch = Bus::findBatch($batchId);$batch->totalJobs; // Total job count$batch->pendingJobs; // Jobs still waiting$batch->failedJobs; // Jobs that failed$batch->progress(); // Completion percentage (0–100)
# Process only the 'emails' queue on Redisphp artisan queue:work redis --queue=emails# Use the database connectionphp artisan queue:work database
queue:work runs continuously. After deploying new code, run php artisan queue:restart to reload workers with the latest changes. In production, use a process manager like Supervisor to keep workers running.
Use the Release middleware when a job should return to the queue instead of running under a particular condition.
use Illuminate\Queue\Middleware\Release;/** * Get the middleware the job should pass through. */public function middleware(): array{ return [ // Release for 60 seconds when the condition is true. Release::when($this->order->isPending(), releaseAfter: 60), ];}
Release::unless() releases the job when the condition is false:
return [ // Release for 60 seconds unless the order has been paid. Release::unless($this->order->isPaid(), releaseAfter: 60),];
Some exceptions indicate that a job should fail immediately rather than be retried. Configure these exception types using dontRetry in bootstrap/app.php:
use App\Exceptions\InvalidPodcastSourceException;use Illuminate\Foundation\Configuration\Exceptions;->withExceptions(function (Exceptions $exceptions): void { $exceptions->dontRetry([ InvalidPodcastSourceException::class, ]);})
For finer control, pass a closure to dontRetryWhen. When the closure returns true, the job is marked as failed immediately without further retries:
Use this for exceptions where retrying would never succeed — for example, validation errors, expired subscriptions, or permanent third-party rejections.
# List all failed jobsphp artisan queue:failed# Retry a specific jobphp artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece# Retry all failed jobsphp artisan queue:retry all# Delete a specific failed jobphp artisan queue:forget ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece# Delete all failed jobsphp artisan queue:flush
<?phpnamespace App\Jobs;use App\Models\Order;use App\Mail\OrderConfirmed;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Queue\Queueable;use Illuminate\Queue\Attributes\Tries;use Illuminate\Queue\Attributes\Timeout;use Illuminate\Support\Facades\Mail;#[Tries(3)]#[Timeout(30)]class SendOrderConfirmation implements ShouldQueue{ use Queueable; public function __construct( public Order $order, ) {} public function handle(): void { Mail::to($this->order->user->email) ->send(new OrderConfirmed($this->order)); } public function failed(?Throwable $exception): void { // Notify the team about the failure }}
3
Dispatch from the controller
use App\Jobs\SendOrderConfirmation;public function store(Request $request): RedirectResponse{ $order = Order::create($request->validated()); SendOrderConfirmation::dispatch($order); return redirect()->route('orders.show', $order) ->with('success', 'Order placed successfully.');}
Set QUEUE_CONNECTION=sync in your .env during development. Jobs run immediately without a worker, making it easy to test the full flow without extra processes.
QUEUE_CONNECTION=sync
Common commands reference
# Start the workerphp artisan queue:work# Restart workers after deploymentphp artisan queue:restart# List failed jobsphp artisan queue:failed# Retry all failed jobsphp artisan queue:retry all# Delete all failed jobsphp artisan queue:flush