How to Run Laravel Queue Workers in Production: Best Practices for High Availability

Run Laravel queue workers as supervised daemons using php artisan queue:work, tune WorkerOptions for memory limits and job timeouts, and leverage the built-in queue:restart signal to enable zero-downtime deployments.

The laravel/framework repository provides a robust queue system centered around the Illuminate\Queue\Worker class. In production environments, maintaining persistent, efficient queue processing requires understanding the daemon architecture, configuring process supervision, and implementing proper restart strategies to prevent job loss during deployments.

Understanding the Laravel Queue Worker Architecture

The queue worker operates as a long-running daemon through the Worker::daemon() method in src/Illuminate/Queue/Worker.php. This method executes a continuous loop of getNextJob → runJob → sleep until a stop condition triggers.

The worker remains responsive to external commands through stateless cache checks. On every iteration, Worker::stopIfNecessary() evaluates multiple shutdown conditions:

  • Memory limits via Worker::memoryExceeded()
  • Restart signals via Worker::queueShouldRestart() checking the illuminate:queue:restart cache key
  • Max job/time constraints via WorkerOptions configurations
  • POSIX signals including SIGTERM, SIGINT, SIGQUIT for shutdown and SIGUSR2, SIGCONT for pause/resume functionality

The Worker::listenForSignals() method registers async signal handlers, allowing the process to handle timeouts through Worker::registerTimeoutHandler() (which sends SIGALRM for job killing) and graceful shutdowns without losing active jobs.

Process Management Strategies for Production

Running workers directly from the command line risks process death during server restarts or application crashes. Production environments require process managers to ensure workers remain alive.

Supervisor remains the industry standard for managing Laravel queue workers. It automatically restarts failed processes and manages worker pools.

Create a configuration file at /etc/supervisor/conf.d/laravel-queue-worker.conf:

[program:laravel-queue-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/myapp/artisan queue:work redis \
    --daemon \
    --queue=high,default \
    --sleep=3 \
    --tries=3 \
    --timeout=60 \
    --memory=256
autostart=true
autorestart=true
stopwaitsecs=3600
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/laravel/queue-worker.log

The stopwaitsecs=3600 directive ensures Supervisor waits up to one hour for the current job to complete before force-killing the process, respecting the graceful shutdown logic in Worker::stopIfNecessary().

Apply the configuration:

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

Systemd Unit Alternative

For systems preferring systemd over Supervisor, create /etc/systemd/system/laravel-queue-worker.service:

[Unit]
Description=Laravel Queue Worker
After=network.target

[Service]
User=www-data
Group=www-data
Restart=always
ExecStart=/usr/bin/php /var/www/myapp/artisan queue:work redis \
    --queue=high,default \
    --sleep=3 \
    --tries=3 \
    --timeout=60 \
    --memory=256

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl enable laravel-queue-worker
sudo systemctl start laravel-queue-worker

Tuning WorkerOptions for Performance

The WorkerOptions class (src/Illuminate/Queue/WorkerOptions.php) defines behavioral parameters passed via command-line arguments to queue:work. Optimize these values based on your workload characteristics:

  • --memory=256: Sets the megabyte threshold for Worker::memoryExceeded(). When exceeded, the worker exits cleanly after the current job, allowing the process manager to respawn a fresh instance.
  • --timeout=60: Configures the maximum seconds a job may run before Worker::registerTimeoutHandler() sends SIGALRM to kill the process.
  • --sleep=3: Controls the seconds to wait between polling the queue when empty. Higher values reduce CPU usage but increase latency.
  • --tries=3: Specifies retry attempts before marking a job as failed.
  • --max-jobs=1000 and --max-time=3600: Enable controlled worker recycling, preventing memory leaks in long-running processes by exiting after processing 1,000 jobs or running for one hour.

Example optimized command:

php artisan queue:work redis \
    --name=high-priority \
    --queue=high,default \
    --sleep=3 \
    --timeout=60 \
    --tries=3 \
    --memory=256 \
    --max-jobs=1000 \
    --max-time=3600

Implementing Graceful Restarts and Zero-Downtime Deployments

The Laravel queue worker supports zero-downtime deployments through a cache-based coordination mechanism. When running php artisan queue:restart, the RestartCommand class (src/Illuminate/Queue/Console/RestartCommand.php) writes a fresh timestamp to the illuminate:queue:restart cache key.

Each worker checks Worker::getTimestampOfLastQueueRestart() on every daemon loop iteration via Worker::queueShouldRestart(). Detecting a change triggers a graceful exit after the current job completes, allowing your deployment script to start new workers with updated code.

Deployment script example:


# Place application in maintenance mode (optional)

php /var/www/myapp/artisan down

# Signal all workers to exit after current job

php /var/www/myapp/artisan queue:restart

# Deploy new code

git pull origin main
composer install --no-dev --optimize-autoloader

# Bring application back online

php /var/www/myapp/artisan up

# Supervisor/systemd automatically starts new workers

This pattern prevents "half-finished" jobs and ensures no worker processes linger with outdated code.

Monitoring Queue Events and Health

The worker fires events through methods like raiseJobProcessing(), raiseJobProcessed(), and raiseJobFailed() in Worker.php. Listen to these events in your application service provider for observability:

use Illuminate\Support\Facades\Event;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobFailed;

Event::listen(JobProcessed::class, function (JobProcessed $event) {
    logger()->info('Job processed', [
        'id' => $event->job->getJobId(),
        'name' => $event->job->resolveName(),
        'connection' => $event->connectionName,
    ]);
});

Event::listen(JobFailed::class, function (JobFailed $event) {
    logger()->error('Job failed', [
        'id' => $event->job->getJobId(),
        'exception' => $event->exception->getMessage(),
    ]);
    
    // Send metrics to monitoring service (Honeycomb, Datadog, etc.)
});

For temporary maintenance without stopping processes, set Cache::put('illuminate:queue:pause', true) to pause processing. The Worker::daemonShouldRun() method checks this cache key, causing workers to sleep until the key clears.

Summary

  • Run workers as supervised processes using Supervisor or systemd to ensure automatic restarts after crashes or memory limit hits.
  • Tune WorkerOptions with appropriate --memory, --timeout, and --max-jobs values to prevent resource exhaustion and memory leaks.
  • Use queue:restart during deployments to trigger graceful shutdowns via the cache-based signal mechanism in Worker::queueShouldRestart().
  • Implement event listeners for JobProcessed and JobFailed to capture metrics and maintain visibility into queue health.
  • Leverage cache-based pausing via illuminate:queue:pause for maintenance windows without killing worker processes.

Frequently Asked Questions

How do I prevent memory leaks in long-running Laravel queue workers?

Memory leaks occur when PHP objects accumulate across thousands of job executions. Configure the --memory option (e.g., --memory=256) to set a megabyte limit, and use --max-jobs (e.g., --max-jobs=1000) to force worker recycling. The Worker::memoryExceeded() method checks consumption on each loop, exiting cleanly when limits breach to allow your process manager to spawn fresh instances.

What happens to active jobs when I run queue:restart?

Active jobs complete normally. The RestartCommand writes a timestamp to the illuminate:queue:restart cache key, which Worker::queueShouldRestart() detects on the next iteration. The worker sets shouldQuit to true and exits after stopIfNecessary() confirms the current job finished, ensuring no jobs terminate mid-execution.

Should I use queue:work or queue:listen in production?

Always use queue:work with the --daemon flag for production. The WorkCommand class (src/Illuminate/Queue/Console/WorkCommand.php) initiates a daemon process that sleeps between jobs, making it significantly more CPU-efficient than queue:listen, which boots the framework anew for every job. Daemon mode requires proper signal handling and process supervision but provides superior performance for high-throughput queues.

How do I handle queue workers during database maintenance windows?

Set Cache::put('illuminate:queue:pause', true, $seconds) before maintenance. The Worker::daemonShouldRun() method checks this cache key each loop iteration, causing workers to sleep rather than poll for jobs. Remove the key or let it expire to resume processing. This approach keeps worker processes alive and responsive to restart signals while preventing job processing during sensitive operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →