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 with Redis: Production Setup Guide

By Kokil Thapa | Last reviewed: August 2026

Slow HTTP responses kill user experience and conversion rates, but moving heavy tasks to the background introduces operational complexity that breaks without proper infrastructure. This Laravel Queues with Redis: Production Setup Guide provides the exact configuration, supervision, and monitoring patterns required to run reliable asynchronous processing in 2026. Whether you are building a legal-tech portal or an eCommerce platform, getting this foundation right prevents silent data loss and stuck workflows.

Background processing is non-negotiable for modern applications. On a recent Laravel development project, we reduced average API response times from 4 seconds to under 200ms simply by offloading document generation and email dispatch to Redis-backed queues. However, the difference between a demo and a production system lies entirely in how you handle worker lifecycle, failure recovery, and observability. The default synchronous driver works locally, but Redis is the only viable choice for production workloads due to its atomic operations, persistence options, and native support for prioritized queues.

How do you configure Laravel Queues with Redis for production reliability?

The default Redis queue configuration in Laravel is functional but insufficient for production traffic. You must explicitly define connection parameters, separate your cache from your queue data, and set appropriate timeouts to prevent zombie jobs.

Separate Queue and Cache Connections

A common mistake I see on client projects is using the same Redis connection for both application caching and queue storage. When you run php artisan cache:clear, you risk wiping pending jobs. Always define a dedicated connection in config/database.php:

<?php // config/database.php 'redis' => [ 'client' => env('REDIS_CLIENT', 'phpredis'), 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ], // Dedicated connection for queues - NEVER share with cache 'queues' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_QUEUE_DB', '1'), ], ],

Configure Queue Connection Parameters

In config/queue.php, point the redis driver to your new connection and set explicit timeouts. The retry_after value must always exceed your longest expected job duration plus a buffer, otherwise Laravel will assume the job died and release it back to the queue while it is still executing.

<?php // config/queue.php 'redis' => [ 'driver' => 'redis', 'connection' => 'queues', // References database.redis.queues 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => 900, // 15 minutes max job time + buffer 'block_for' => 5, // Blocking pop instead of polling 'after_commit' => true, // Dispatch only after DB transaction commits ],

The after_commit flag is critical. Without it, jobs can be dispatched and processed before the database transaction that created them has actually committed, leading to "model not found" errors in workers. Setting this globally in 2026 is considered best practice for any Laravel application using database transactions alongside queues.

Laravel App ServerHTTP Request / Commanddispatch(new Job())after_commit: trueRedis (DB 1)queue:defaultqueue:emailsqueue:reportsfailed_jobsWorker Pool ASupervisor • 4 processesqueue:default,emailsWorker Pool BSupervisor • 2 processesqueue:reports
Laravel Queues with Redis production architecture: dedicated Redis DB, separated queues, and supervised worker pools

How do you supervise Laravel queue workers to prevent downtime?

Queue workers are long-lived processes that will eventually crash, run out of memory, or encounter fatal errors. Running php artisan queue:work manually in a terminal is never acceptable in production. You need a process supervisor that automatically restarts failed workers and manages concurrency.

Supervisor Configuration

Supervisor remains the most widely used tool for managing Laravel workers on Ubuntu servers. Install it via sudo apt install supervisor and create a configuration file at /etc/supervisor/conf.d/laravel-worker.conf:

[program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --max-jobs=1000 --memory=256 autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=4 redirect_stderr=true stdout_logfile=/var/log/laravel-worker.log stopwaitsecs=30

Key flags explained from real deployment experience:

  • --max-jobs=1000: Automatically restart the worker after processing 1000 jobs. This prevents memory leaks from accumulating over days of uptime.
  • --max-time=3600: Restart after one hour regardless of job count. Acts as a safety net alongside max-jobs.
  • --memory=256: Hard memory limit in MB. Workers exceeding this are gracefully terminated and restarted.
  • --sleep=3: Seconds to sleep when no jobs are available. With block_for enabled, this acts as a fallback timeout rather than active polling.
  • --tries=3: Default retry attempts per job. Individual jobs can override this with a $tries property.

After creating the config, reload Supervisor:

sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start laravel-worker:*

Systemd Alternative for Modern Deployments

If your infrastructure uses systemd exclusively and you want to avoid installing additional packages, you can create a service template. This approach integrates better with journalctl logging and cgroup resource limits. Create /etc/systemd/system/laravel-queue@.service:

[Unit] Description=Laravel Queue Worker %i After=redis.service mysql.service [Service] User=www-data Group=www-data WorkingDirectory=/var/www/html ExecStart=/usr/bin/php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-jobs=1000 --memory=256 Restart=always RestartSec=5 MemoryMax=300M [Install] WantedBy=multi-user.target

Enable four instances with systemctl enable --now laravel-queue@{1..4}.service. In my experience managing deployments across multiple Nepal-based legal-tech portals, Supervisor offers simpler debugging for junior team members, while systemd provides tighter OS integration for experienced DevOps engineers.

How do you handle failed jobs and implement retry strategies?

Jobs fail. External APIs timeout, validation rules change, third-party services return unexpected responses. Your production setup must handle failures gracefully without losing data or flooding logs with noise.

Failed Job Storage

Always configure failed job storage. Redis works for high-throughput systems, but for most applications I recommend the database driver because it allows easy inspection via admin panels and SQL queries:

// config/queue.php 'failed' => [ 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 'database' => env('DB_CONNECTION', 'mysql'), 'table' => 'failed_jobs', ],

Run php artisan queue:failed-table followed by migration. The database-uuids driver is preferred over the legacy database driver because UUIDs prevent ID collision issues during replication or multi-environment restores.

Exponential Backoff and Conditional Retries

Flat retry intervals waste resources. Implement exponential backoff directly on your job classes:

<?php class SendLegalDocument implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 5; public function backoff(): array { // Retry after 1min, 5min, 15min, 30min, 1hr return [60, 300, 900, 1800, 3600]; } public function retryUntil(): DateTime { // Absolute deadline: 24 hours from initial dispatch return now()->addHours(24); } public function failed(?Throwable $exception): void { // Notify admin, update model status, log to Sentry Notification::route('slack', config('services.slack.alerts')) ->notify(new LegalDocumentFailed($this->documentId, $exception)); } }

The retryUntil method takes precedence over $tries. This is essential for time-sensitive operations like court filing notifications where retrying after a deadline is pointless. On a notary service portal I maintain, we use this pattern to ensure attestation reminders stop retrying once the appointment window has passed.

Job DispatchedAttempt 1Process JobExecute handle()Success?YesCompletedRemove from queueNoTries < Max?Within retryUntil?YesWait Backoff60s → 300s → 900sRe-queueNoFailed PermanentlyStore + Alert
Job retry flow with exponential backoff: conditional retries respect both try count and absolute deadline

Should you use Laravel Horizon or manual monitoring for Redis queues?

For any production system processing more than a few hundred jobs daily, Laravel Horizon is effectively mandatory. Manual Redis CLI inspection does not scale and provides no historical metrics.

CriteriaLaravel HorizonManual / CLI Monitoring
Real-time throughput metricsBuilt-in dashboard with graphsRequires custom scripts or external tools
Worker auto-scalingConfigurable min/max processes per queueStatic Supervisor numprocs only
Failed job inspectionWeb UI with payload viewer and retry buttonphp artisan queue:failed table output
Tag-based filteringTrack jobs by model, user, or tenantNot possible without custom instrumentation
Historical performance dataRetained for configurable period (default 7 days)None unless piping to external metrics store
Setup complexityComposer require + publish config + deployZero additional dependencies
Production overheadAdditional Redis keys + master processNegligible

Install Horizon with composer require laravel/horizon and publish its configuration. The key production setting is defining separate supervisors for different workload profiles:

// config/horizon.php 'environments' => [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default', 'emails'], 'balance' => 'auto', 'minProcesses' => 2, 'maxProcesses' => 10, 'maxTime' => 3600, 'maxJobs' => 1000, 'memory' => 256, 'tries' => 3, ], 'supervisor-reports' => [ 'connection' => 'redis', 'queue' => ['reports'], 'balance' => 'simple', 'processes' => 2, 'maxTime' => 3600, 'memory' => 512, 'tries' => 1, ], ], ],

The auto balance mode dynamically adjusts worker count based on queue depth, which is invaluable during traffic spikes. For report generation queues that are memory-intensive but low-priority, I use simple balancing with fixed processes to prevent them from starving transactional jobs.

Laravel Horizon Master ProcessMonitors queue depth every 3 seconds • Adjusts workers within min/max boundsLow LoadQueue depth < 102 Workers Active (min)Medium LoadQueue depth 10–1005 Workers ActiveHigh LoadQueue depth > 10010 Workers Active (max)Scaling Decision Logicif (pendingJobs > currentWorkers * cooldownThreshold) → scale upif (pendingJobs < currentWorkers * 0.5 for 60s) → scale downNever exceed maxProcesses • Never drop below minProcesses
Horizon auto-scaling adjusts worker count between configured min and max based on real-time queue depth

What are the common production pitfalls with Laravel Redis queues?

After maintaining queue infrastructure for over a decade, certain failure modes appear repeatedly. Avoiding these saves hours of 2 AM debugging.

  1. Deploying without restarting workers. Queue workers cache the entire application in memory. After every deployment, you must run php artisan queue:restart or rely on Horizon's automatic restart signal. Forgetting this means old code continues processing jobs indefinitely.
  2. Storing large payloads in Redis. Redis is memory-resident. Passing entire Eloquent models or base64-encoded files as job properties will exhaust memory under load. Always pass only IDs and re-fetch inside handle(). Use Spatie Media Library's queued conversions for file processing instead of embedding binary data.
  3. Missing idempotency guards. Network blips cause duplicate dispatches. Every job that modifies external state must check whether it has already been processed. A simple pattern is storing a processed flag keyed by job UUID before executing side effects.
  4. Ignoring Redis persistence configuration. If Redis restarts with default volatile-only persistence, all queued jobs vanish. Ensure appendonly yes (AOF) or RDB snapshots are configured in redis.conf for production instances. For critical legal-tech workflows, I use AOF with appendfsync everysec.
  5. Running workers on the same server as Redis under heavy load. Queue workers compete for CPU and memory with Redis itself. On budget-constrained Nepal hosting environments, this is sometimes unavoidable, but monitor Redis latency closely. If p99 latency exceeds 5ms, separate the services.

For teams building API-driven systems, understanding how queues interact with your broader architecture is essential. My article on Laravel API best practices covers patterns for returning immediate responses while queuing deferred work, including proper HTTP 202 Accepted semantics and status polling endpoints.

Conclusion

This Laravel Queues with Redis: Production Setup Guide covers the configuration, supervision, failure handling, and monitoring foundations that separate reliable background processing from fragile prototypes. Separate your Redis connections, supervise every worker with Supervisor or systemd, implement exponential backoff with absolute deadlines, and deploy Horizon for visibility. These are not optional optimizations — they are requirements for any system that processes real business transactions asynchronously.

If your team needs help auditing an existing queue setup, migrating from database drivers to Redis, or building a greenfield async architecture for a legal-tech or eCommerce platform, get in touch. I have shipped and maintained production queue infrastructure for Nepali businesses since 2010 and can help you avoid the costly mistakes that only surface under real traffic.

Frequently Asked Questions

Redis handles high-throughput job processing without locking your primary MySQL or PostgreSQL tables. Database drivers cause row-level contention under load, while Redis uses atomic operations designed specifically for message brokering in production environments.

Redis 7.x is recommended for Laravel 12. It supports required stream commands and ACL features. Older versions like 6.x work but lack performance optimizations and security controls needed for reliable production queue management.

A basic managed Redis instance costs Rs 3,000–5,000 monthly (~USD 22–37). Self-hosted on a 2GB VPS runs Rs 1,500–2,500 (~USD 11–18). Budget at least 2GB RAM for stable queue performance alongside your application server.

Set persistent=true in config/database.php redis options to reuse connections across requests. Configure read_write_timeout to 60 seconds minimum. On production servers running PHP-FPM, enable connection pooling via phpredis extension rather than predis to reduce overhead during high-volume job dispatching.

Jobs are lost unless you enable retry mechanisms. Configure failed_jobs table in Laravel and set --tries=3 with --backoff=60 on workers. Use Redis RDB persistence with save 900 1 directive. For critical jobs like payment webhooks on projects similar to Nepal Gift Card, implement application-level idempotency checks.

Systemd is preferred on Ubuntu 22/24 servers. Create /etc/systemd/system/laravel-queue.service with Restart=always and MemoryMax=512M. Supervisord works but adds unnecessary complexity. In my experience managing deployments via Deployer 7, systemd integrates better with zero-downtime releases and automatic PHP-FPM reloads.

Always set --max-jobs=1000 and --max-time=3600 flags on worker processes. This forces graceful restarts before memory accumulates. Monitor with htop and configure systemd MemoryMax limits. On legal-tech portals handling document generation, I've seen workers consume 2GB+ without these constraints, eventually crashing the entire queue pipeline.

Technically yes, but separate databases (SELECT 0 for cache, SELECT 1 for queues) at minimum. Better practice is dedicated Redis instances. Cache eviction policies can accidentally purge pending jobs. For client projects on shared EC2 infrastructure, I always isolate queue Redis to prevent cache flushes from disrupting background job processing.

Query the failed_jobs table directly or build a simple admin dashboard using Laravel's built-in queue:failed-table migration. Use queue:retry-batch for bulk retries. Horizon requires additional dependencies and Redis streams. For smaller deployments like service business sites, direct database monitoring with scheduled cleanup jobs provides sufficient visibility without extra infrastructure overhead.

Check Redis bind address in /etc/redis/redis.conf matches your application server IP. Verify UFW allows port 6379 if remote. Confirm requirepass matches REDIS_PASSWORD in .env. Test connectivity with redis-cli -h host -p 6379 ping. Misconfigured firewall rules after server migration are the most frequent cause I encounter during production deployments.

Set explicit timeout values on HTTP clients within jobs, never rely on defaults. Configure --timeout=300 on workers for slow third-party APIs like payment gateways. Implement exponential backoff with RetryableHttpException handling. On eCommerce projects integrating eSewa or Khalti callbacks, I wrap API calls in try-catch blocks with specific timeout exceptions to prevent indefinite worker hangs.

Only if you require automatic failover and cannot tolerate downtime. Single-node Redis with RDB/AOF persistence suffices for most Nepal-based business applications. Sentinel adds operational complexity. Evaluate based on SLA requirements. For legal service portals where brief queue delays are acceptable during maintenance windows, single-node with proper backups provides adequate reliability at lower cost.

Use multiple queues with weighted priorities: php artisan queue:work --queue=critical,default,low. Dispatch urgent jobs like order confirmations to critical queue. Configure separate worker processes per priority level. On booking systems similar to Adventure Third Pole Trek, payment confirmations run on dedicated critical workers while report generation uses low-priority workers with resource limits.

Enable requirepass authentication and rename dangerous commands like FLUSHALL in redis.conf. Bind to private network interfaces only, never 0.0.0.0. Use TLS encryption for cross-datacenter communication. Configure Redis ACLs for granular permissions. On shared hosting environments, isolate Redis sockets with proper file permissions. Never expose Redis publicly without VPN or SSH tunnel access.

Switch when any task exceeds 200ms or user-facing response times degrade. Email sending, PDF generation, image processing, and third-party API calls must be queued immediately. Sync execution blocks HTTP responses. For new Laravel 12 projects, configure Redis queues from day one even if volume is low. Retrofitting later requires testing all job serialization and dependency injection patterns.

Share this article

Quick Contact Options
Choose how you want to connect me: