Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Queues and Jobs: Background Processing Explained

By Kokil Thapa | Last reviewed: September 2026

Your checkout page should not wait for a payment gateway, three SMS providers, and a PDF generator to finish before showing a thank-you screen. That is where Laravel Queues and Jobs: Background Processing Explained becomes practical architecture, not optional polish. On real client projects — booking systems, eCommerce carts, legal-tech portals with document workflows — I move slow or failure-prone work off the HTTP request path. Laravel 12 and 13 ship a mature queue layer backed by Redis production queue setup patterns most teams already run. This guide walks through job classes, drivers, workers, retries, and the mistakes that break deployments.

What are Laravel queues and jobs used for in background processing?

A job is a PHP class that implements work you want deferred. A queue is the transport layer that stores serialized jobs until a worker picks them up. The controller dispatches; the worker executes. That split is the core of background processing in Laravel.

Typical candidates include sending transactional email, calling third-party APIs, generating invoices, resizing uploads, syncing inventory, and pushing webhook payloads. On a production enterprise Laravel application, anything that can fail independently or take more than a few hundred milliseconds belongs off the request thread.

Laravel Queue ArchitectureHTTP RequestControllerJob DispatchSerializeQueue StoreRedis / DBFast Response200 OKQueue Worker Processphp artisan queue:workhandle() runs outside HTTPRetries, timeouts, failed_jobs
Laravel Queues and Jobs: Background Processing Explained — HTTP dispatches work; workers consume it asynchronously.

Laravel serializes the job payload — class name, constructor arguments, and metadata — into the configured driver. Workers poll the driver, deserialize the job, and call the handle() method. If the job throws, Laravel applies your retry policy or records a failure.

Do not confuse queues with the scheduler. Cron-driven schedule:run tasks fire on a timetable. Queues react to application events. The distinction matters when you design idempotent jobs versus periodic cleanup. Read Laravel cron job vs queue worker before mixing both on the same server without a plan.

Jobs vs events vs notifications

Events broadcast that something happened. Listeners may dispatch jobs. Notifications can implement ShouldQueue and become queued mail or SMS automatically. Keep one job per bounded unit of work — "send booking confirmation" beats "process entire order lifecycle".

How do you create and dispatch a Laravel job class?

Start with an Artisan generator. Laravel 12 and 13 both support the same pattern on PHP 8.2 or higher (PHP 8.3+ for Laravel 13).

php artisan make:job SendBookingConfirmation

<?php

namespace App\Jobs;

use App\Models\Booking;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class SendBookingConfirmation implements ShouldQueue
{
    use Queueable;

    public function __construct(public Booking $booking) {}

    public function handle(): void
    {
        /* mail, SMS, PDF generation */
    }
}

Dispatch from a controller, listener, or service:

SendBookingConfirmation::dispatch($booking);

SendBookingConfirmation::dispatch($booking)
    ->onQueue('notifications')
    ->delay(now()->addMinutes(5));

The ShouldQueue interface tells Laravel not to run synchronously when a non-sync driver is active. Without it, the class still dispatches but executes inline — a common surprise during local development with QUEUE_CONNECTION=sync.

Pass only serializable data

Eloquent models work because Laravel stores the model ID and rehydrates via SerializesModels. Closures, open file handles, and raw PDO connections do not survive serialization. Pass IDs or DTOs when jobs cross long delays.

Form requests and validation still belong on the web layer

Validate input before dispatch. Jobs should assume the payload is already clean. Re-validate inside handle() only when stale data is a real risk — for example, rechecking stock before fulfilment an hour after checkout.

Which queue driver should you use for Laravel background jobs?

Your driver choice affects throughput, operational cost, and failure visibility. Most production Laravel apps I maintain use Redis 8.10 with a dedicated queue database index. Database queues are fine for low volume or shared hosting without Redis.

DriverBest forTrade-offs
syncLocal dev, testsNo background processing; blocks HTTP
databaseSmall apps, no RedisPolling load on MySQL 9.7; slower at scale
redisMost production Laravel appsNeeds Redis + worker supervision
sqsAWS-hosted, multi-regionVendor lock-in, IAM setup, per-request cost
beanstalkdLegacy stacksLess common in new 2026 projects

Configure in .env and config/queue.php:

QUEUE_CONNECTION=redis

REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Run migrations for database-backed queues and failed-job storage:

php artisan queue:table
php artisan queue:failed-table
php artisan migrate

For high-traffic booking platforms like Adventure Third Pole Trek, Redis plus Horizon gives visibility into wait times and throughput. See Laravel Horizon monitoring guide when you outgrow a single worker process.

Queue Driver DecisionLow traffic?Yesdatabase driverNoOn AWS?SQS driverSelf-hostedVPS / EC2redis + HorizonProduction default: Redis queue on PHP 8.3+Supervisor or systemd keeps workers alive
Choosing a queue driver for Laravel background jobs — database for small apps, Redis for most production workloads.

How do you run Laravel queue workers in production?

Dispatching jobs is half the system. Without a running worker, Redis lists grow and users never receive email. Every deployment I handle includes worker supervision alongside PHP-FPM.

  1. Install and configure the queue driver (Redis recommended).
  2. Run php artisan queue:work redis --queue=default,notifications --tries=3 --timeout=90.
  3. Wrap the worker in Supervisor or systemd so it restarts on crash or deploy.
  4. Reload workers after code deploy — stale opcache plus old code causes silent mismatches.
  5. Monitor queue depth, failed jobs, and worker memory.

Example Supervisor program block on Ubuntu 24:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopwaitsecs=3600
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/app/shared/storage/logs/worker.log

After Deployer symlink swap, run php artisan queue:restart. Workers finish the current job, then exit; Supervisor starts fresh processes with new code. I use this same pattern on sister legal-tech sites sharing a GitLab CI pipeline. Details sit in Linux system administration and Horizon vs Supervisor comparison.

Multiple queues and priority

Split traffic by concern: default, notifications, webhooks, reports. Workers read queues left-to-right:

php artisan queue:work redis --queue=critical,default,low

Payment callbacks and OTP SMS belong on critical. Monthly CSV exports belong on low. Without separation, a bulk export can delay password-reset email.

Horizon for Redis at scale

Laravel Horizon adds a dashboard, balancing, and metrics on top of Redis queues. It fits apps processing thousands of jobs per hour — eCommerce order hooks, directory sync, or payment gateway callbacks. Horizon is Redis-only; do not install it if you stay on the database driver.

How do you handle failed jobs, retries, and timeouts?

Background processing fails — APIs time out, SMTP rejects mail, disks fill up. Laravel retries thrown exceptions up to your $tries property or CLI flag. After exhaustion, the job lands in failed_jobs.

class ProcessWebhook implements ShouldQueue
{
    public int $tries = 5;
    public int $timeout = 120;
    public array $backoff = [10, 30, 60, 120, 300];

    public function handle(): void
    {
        /* call external API */
    }

    public function failed(?\Throwable $e): void
    {
        /* alert ops, log context */
    }
}

Inspect and replay failures:

php artisan queue:failed
php artisan queue:retry all
php artisan queue:flush

Make jobs idempotent. A retry must not double-charge a card or send duplicate SMS. Store a processed flag, use gateway idempotency keys, or wrap state changes in database transactions. For bulk imports, combine queues with Laravel job batching so you track partial completion.

Job Retry LifecycleDispatchWorkerSuccessJob deletedExceptionRetry count++Retries exhausted?failed() hook runsRow stored in failed_jobsqueue:retry replays manuallyRetry
Failed Laravel queue jobs retry with backoff, then land in failed_jobs for inspection and replay.

Unique jobs and overlap prevention

Laravel supports ShouldBeUnique to prevent duplicate concurrent jobs for the same resource — useful when users double-click "Resend receipt". Combine with WithoutOverlapping middleware when two workers must not process the same entity simultaneously.

When should you queue work instead of running it synchronously?

Queue when the user does not need the result immediately. Keep synchronous code for auth checks, cart totals, and anything that gates the next screen.

  • Queue: email, SMS, webhooks, image processing, search indexing, PDF generation, analytics pings.
  • Stay synchronous: login, permission checks, stock reservation at checkout, payment authorization response.
  • Either: small cache warming — queue if it touches remote services; inline if it is a local Redis read.

On a legal-tech portal, document virus scanning and thumbnail generation belong in queues. Form validation and fee calculation stay on the request. The same split applies to grocery delivery order routing and REST API endpoints that must return within gateway timeout limits. Compare with background jobs vs cron design when work is time-triggered rather than event-triggered.

For debugging payloads during development, paste job JSON into the JSON formatter tool before blaming production Redis. Structured logs beat guessing serialized shape.

eCommerce Order Queue ExampleOrder PlacedSync: payment OKSendReceiptJobnotificationsSyncInventoryJobdefault queueWebhookJobwebhooks queueIndexSearchJoblow priorityUser sees thank-you in 200ms
Laravel Queues and Jobs split eCommerce post-checkout work across named queues by priority.

Testing queued jobs

Use Queue::fake() in feature tests to assert dispatch without running workers:

Queue::fake();

$response = $this->post('/orders', $payload);

Queue::assertPushed(SendBookingConfirmation::class);

Run integration tests with QUEUE_CONNECTION=sync sparingly — it hides race conditions that only appear with real async workers.

Performance and database load

Queued jobs still hit your database. Eager-load relationships inside handle() to avoid N+1 queries. Offload heavy reads to read replicas only when your infrastructure supports it. PostgreSQL 18 and MySQL 9.7 both behave well as job payload stores when indexed correctly — see PostgreSQL for Laravel developers for connection pooling notes under worker load.

Official reference: the Laravel queue documentation covers drivers, workers, and failure handling. Redis client details live in the Redis PHP client docs.

Key Takeaways

  • Implement ShouldQueue on job classes and dispatch early — keep HTTP responses under gateway timeouts.
  • Use Redis as the default production driver; reserve the database driver for low-volume or Redis-less hosting.
  • Supervise workers with Supervisor or systemd and run queue:restart on every deploy.
  • Design jobs to be idempotent with explicit $tries, $timeout, and $backoff values.
  • Split named queues by priority so bulk work cannot block OTP or payment callbacks.
  • Monitor failed_jobs and queue depth — silent worker death is the most common production queue failure.

People Also Ask

What is the difference between dispatch() and dispatchSync() in Laravel?

dispatch() sends the job to the configured queue connection for asynchronous processing. dispatchSync() runs handle() immediately inside the current process, bypassing the queue — useful for tests or rare cases where you need inline execution of a job class.

Do Laravel queues require Redis?

No. Laravel supports sync, database, Redis, SQS, Beanstalkd, and other drivers. Redis is the most common production choice because it is fast and pairs with Horizon, but the database driver works for smaller apps without extra infrastructure.

How many queue workers do I need?

Start with one worker per CPU core for CPU-bound jobs, or two to four workers for I/O-bound API calls. Watch queue wait time and scale horizontally. Horizon auto-balancing simplifies this for Redis deployments.

What happens if no queue worker is running?

Jobs accumulate in the queue store but never execute. The web app still returns success to users, yet email, webhooks, and exports silently stop. Always supervise workers and alert on queue depth thresholds.

Ship background processing you can trust in production

Laravel Queues and Jobs: Background Processing Explained comes down to a simple contract: dispatch bounded work, supervise workers, and design for retries. That pattern has kept checkout flows fast on eCommerce builds and document pipelines reliable on legal-tech portals I maintain. If your app dispatches jobs but workers die after deploy, or failed webhooks pile up unnoticed, the fix is usually operational — not another package. For scaling patterns, read mastering Laravel queues for high traffic. Need queue architecture reviewed on an existing Laravel 12 or 13 codebase? Contact us or explore ongoing support and maintenance. Browse the portfolio for production apps that rely on background jobs daily, or review Laravel API best practices for timeout-safe endpoint design.

Frequently Asked Questions

A job is a PHP class that implements deferred work; a queue is the transport layer storing serialized jobs until a worker executes them. The controller dispatches, the worker runs handle(). Typical uses include transactional email, third-party API calls, invoice generation, image resizing, inventory sync, and webhooks. On production Laravel apps, anything that can fail independently or takes more than a few hundred milliseconds belongs off the HTTP request thread.

dispatch() sends the job to the configured queue connection for asynchronous processing. dispatchSync() runs handle() immediately in the current process, bypassing the queue entirely.

Run php artisan make:job SendBookingConfirmation, implement ShouldQueue, and use the Queueable trait. Pass serializable data — Eloquent models work via SerializesModels, but closures and open file handles do not. Dispatch with SendBookingConfirmation::dispatch($booking), optionally chaining onQueue('notifications') or delay(). Validate input on the web layer before dispatch; jobs should assume the payload is already clean unless stale data is a real risk.

Redis is the default choice for most production Laravel apps — fast throughput and pairs with Horizon for monitoring. The database driver suits low-volume apps or shared hosting without Redis, though it adds polling load on MySQL 9.7. Use sync for local dev and tests, SQS for AWS-hosted multi-region setups, and Beanstalkd only on legacy stacks. Configure via QUEUE_CONNECTION in .env and run queue:table plus queue:failed-table migrations if using the database driver.

No. Laravel supports sync, database, Redis, SQS, and Beanstalkd drivers. Redis is the most common production choice, but the database driver works for smaller apps without extra infrastructure.

Install your queue driver, then run php artisan queue:work redis --queue=default,notifications --tries=3 --timeout=90. Wrap workers in Supervisor or systemd so they restart on crash or deploy. On Ubuntu 24, configure a program block pointing to your Deployer symlink path, set numprocs for parallel workers, and log to storage/logs/worker.log. After every deploy symlink swap, run php artisan queue:restart so workers finish the current job and reload with fresh code.

Start with one worker per CPU core for CPU-bound jobs, or two to four for I/O-bound API calls. Watch queue wait time and scale horizontally. Horizon auto-balancing simplifies this for Redis deployments.

Jobs accumulate in the queue store — Redis lists or database tables — but never execute. The web app still returns success to users, yet email, webhooks, and exports silently stop. This is the most common production queue failure. Always supervise workers with Supervisor or systemd and alert on queue depth thresholds so worker death does not go unnoticed for hours.

Set public int $tries, $timeout, and array $backoff on your job class. Laravel retries thrown exceptions up to the limit, then stores the job in failed_jobs. Inspect with php artisan queue:failed, replay with queue:retry all, or clear with queue:flush. Implement a failed() method for ops alerts. Design jobs to be idempotent — retries must not double-charge cards or send duplicate SMS. Use processed flags, gateway idempotency keys, or database transactions.

Queue when the user does not need the result immediately: email, SMS, webhooks, image processing, search indexing, PDF generation, and analytics pings. Stay synchronous for auth checks, cart totals, stock reservation at checkout, and payment authorization responses. On legal-tech portals, document virus scanning and thumbnail generation belong in queues; form validation and fee calculation stay on the request. The same split applies to eCommerce post-checkout flows and REST API endpoints that must return within gateway timeout limits.

Queues react to application events — a controller dispatches a job when something happens, and a worker picks it up asynchronously. The scheduler runs cron-driven schedule:run tasks on a fixed timetable. The distinction matters when designing idempotent event-triggered jobs versus periodic cleanup tasks. Mixing both on the same server without a plan can cause resource contention; treat them as separate operational concerns.

Use Queue::fake() in feature tests to assert dispatch without executing handle(): call Queue::fake(), perform the HTTP action, then Queue::assertPushed(SendBookingConfirmation::class). Avoid relying on QUEUE_CONNECTION=sync for integration tests — it hides race conditions that only surface with real async workers and multiple concurrent processes.

Horizon adds a dashboard, auto-balancing, and throughput metrics on top of Redis queues. It fits apps processing thousands of jobs per hour — eCommerce order hooks, directory sync, or payment gateway callbacks. Horizon is Redis-only; do not install it if you stay on the database driver. For high-traffic booking platforms, Redis plus Horizon gives visibility into wait times that a single unsupervised worker process cannot provide.

Laravel supports ShouldBeUnique to block duplicate concurrent jobs for the same resource — useful when users double-click Resend receipt. Combine it with WithoutOverlapping middleware when two workers must not process the same entity simultaneously. Without these guards, retries and user actions can trigger duplicate SMS, emails, or payment operations.

Split traffic by concern — default, notifications, webhooks, reports — and assign workers to read queues left-to-right: php artisan queue:work redis --queue=critical,default,low. Payment callbacks and OTP SMS belong on critical; monthly CSV exports belong on low. Without separation, a bulk export can delay password-reset email for every user on the platform. Named queues keep failure-prone or slow work from blocking time-sensitive operations.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: