Laravel Queue Jobs Not Processing in Production: Fix

Jobs pile up in your Laravel queue and nothing runs? Check the worker, the connection name, stale code after deploy, and the failed jobs table, in that order.

Laravel queue jobs stop processing in production for four main reasons: no worker process is running, the worker reads a different connection or queue than your code writes to, the worker is running old code after a deploy, or the jobs are failing and sitting in the failed_jobs table. Run supervisorctl status first, then compare the connection names, then run php artisan queue:restart after every deploy.

On your laptop you run php artisan queue:work in a terminal and it works. In production nobody keeps that terminal open. The process dies on a server restart, a crash, or a closed SSH session, and Laravel does not warn you. Emails, invoices and webhooks just stop. This guide walks through the causes in the order that finds the problem fastest.

Step 1: Check That a Worker Is Actually Running

Start with the simplest question. Is a queue:work process alive?

ps aux | grep "queue:work"
supervisorctl status

If you see nothing, your jobs have no one to run them. A queued job is only a row in the database or a key in Redis until a worker picks it up.

The Laravel queue docs recommend Supervisor to keep workers alive. A minimal config in /etc/supervisor/conf.d/laravel-worker.conf:

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

Then load it:

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

--max-time=3600 makes each worker exit after an hour so Supervisor restarts it fresh. This limits slow memory growth in long-lived PHP processes.

Step 2: Make Sure the Connection and Queue Names Match

A common trap is a worker that is healthy but reading the wrong place. Your .env might say:

QUEUE_CONNECTION=database

But the Supervisor command says queue:work redis. The command argument wins. Your app writes to the jobs table and the worker listens to Redis. Nothing ever matches. This DEV post describes exactly this case.

The same applies to queue names. If you dispatch with ->onQueue('emails'), the worker must listen to it:

php artisan queue:work redis --queue=emails,default

Without --queue=emails, the worker only reads default, and your email jobs wait forever. Check both sides:

php artisan tinker
>>> config('queue.default')

Step 3: Restart Workers After Every Deploy

A worker loads your application code once and keeps it in memory. After you deploy new code, the worker still runs the old version. Jobs may fail with missing classes or behave in outdated ways.

Add this to your deploy script:

php artisan queue:restart

This tells workers to finish their current job and exit. Supervisor then starts new ones with the new code. It depends on your cache driver working, because the restart signal is stored in cache. If your cache is misconfigured, the signal never arrives. The DEV article on restarting queue workers safely covers this in more detail.

Step 4: Look at Failed Jobs

Jobs may not be stuck at all. They may be failing and being parked.

php artisan queue:failed
php artisan queue:retry all

The failed_jobs table needs to exist. If it does not, create it:

php artisan queue:failed-table
php artisan migrate

(On newer Laravel versions the table may already be in your migrations, so check before adding another.) Read the exception column. It usually names the real cause: a missing environment variable, a timeout, or a third-party API error.

Step 5: Check Timeouts and Retry Settings

Two settings cause jobs to vanish or loop:

  • The job $timeout must be shorter than the queue connection's retry_after value in config/queue.php. If retry_after is shorter, the queue assumes the job died and starts it again while the first run is still going. You get duplicate emails and duplicate charges.
  • Set $tries and $backoff so a broken job does not retry in a tight loop.
class SendInvoice implements ShouldQueue
{
    public $tries = 3;
    public $timeout = 120;
    public $backoff = [10, 60, 300];
}

The Laravel docs explain the rule: keep --timeout several seconds shorter than retry_after.

Step 6: Check Redis or Database Health

If workers run and names match but nothing moves, check the backend. For Redis, confirm the app and worker use the same host and database number. For the database driver, confirm the jobs table exists and the worker can reach the database. Locked rows from long transactions can also stall pickup on busy tables.

redis-cli -n 0 llen queues:default

A growing number means jobs are arriving but not leaving. A zero means they were never written, so look at your dispatch code or the QUEUE_CONNECTION=sync value, which runs jobs inline and never queues them.

Step 7: Alert When the Queue Stops

Every fix above is manual. The real problem is that you find out when a customer complains. Add a check that fails loudly: a scheduled job that writes a heartbeat, or a queue size check that alerts when the backlog grows. Our guide to monitoring BullMQ failed jobs covers the same idea for Node.js, and the ideas carry over to Laravel.

How Relia Helps

A stopped queue shows up as a symptom: orders without receipts, signups without welcome emails. Relia watches the users, APIs, database and servers of a live product and joins related signals into one incident explained in plain words, with who was affected. Laravel apps are detected by npx relia-sdk@latest init, and there is also a Composer package for PHP and Laravel. You can add uptime checks on key pages. There is a Free plan, and a live demo at app.tryrelia.com/demo.

FAQ

Why is my Laravel queue worker running but not processing jobs?

Most often the worker listens to a different connection or queue name than your code dispatches to. Compare QUEUE_CONNECTION, the worker command and onQueue().

Do I need to restart queue workers after deploying?

Yes. Workers keep old code in memory. Run php artisan queue:restart in your deploy script.

How do I see why a Laravel job failed?

Run php artisan queue:failed and read the exception stored in the failed_jobs table. Retry with php artisan queue:retry all once the cause is fixed.

Should I use Supervisor or Laravel Horizon?

Horizon is a dashboard and supervisor for Redis queues. It still needs a process manager to keep php artisan horizon alive. For a small app with the database driver, plain Supervisor is enough.

All guides