Recovering Broken Laravel Queues and Scheduled Jobs
Queues and the scheduler fail quietly: no error page, no support ticket, just features that stopped happening. Here is how to find out what is broken, drain the backlog safely, and stop it recurring.
Nothing in Laravel fails as quietly as a queue. The site stays up, pages render, and nobody notices for weeks that welcome emails, invoice generation, webhook processing and nightly reports all stopped. By the time someone complains, there are 40,000 rows in failed_jobs and nobody knows which are safe to retry.
This is the recovery sequence we use.
1. Find out what is actually running
Start on the production host, not in the code:
php artisan queue:monitor default,emails,webhooks
php artisan schedule:list
ps aux | grep 'queue:work'
sudo systemctl status supervisor
crontab -lThe three failure modes you are looking for:
- No worker process. Supervisor never restarted after a reboot or deploy, or the app was deployed with
queue:workrun manually inside a long-dead SSH session. - No cron entry. The scheduler needs exactly one
* * * * * php artisan schedule:runentry. Without it, every scheduled task is fiction. - Workers running stale code.
queue:workboots the framework once. If your deploy never callsqueue:restart, workers keep executing the code from whenever they started.
2. Read the failed jobs before you retry anything
php artisan queue:failedDo not mass-retry. Group failures by exception and by job class first:
select payload::json->>'displayName' as job,
left(exception, 120) as reason,
count(*), min(failed_at), max(failed_at)
from failed_jobs group by 1, 2 order by 3 desc;That single query usually reveals the story: one deploy broke one job class, the retries backed up behind it, and everything after that is collateral. Retrying blindly can double-charge customers, re-send months of email, or re-fire webhooks against partners.
3. Decide per group: retry, discard, or replay carefully
- Idempotent jobs (recalculating a report, syncing a cached value) — safe to retry.
- Side-effecting jobs (emails, payments, webhooks) — check whether the effect already happened, then discard or replay a subset by id:
php artisan queue:retry <uuid>. - Jobs whose payload no longer deserialises because the model was deleted or the class renamed — discard. They cannot succeed.
Throttle replays. Dumping 20,000 jobs onto a live worker pool will take the database or the mail provider down with it.
4. Fix the root causes
Serialisation. Jobs serialise models by id and re-fetch on run. Deleted records throw ModelNotFoundException. Add $deleteWhenMissingModels = true where the correct behaviour is to skip.
Timeouts and memory. Set $timeout, $tries and $backoff explicitly on each job class. Make the Supervisor stopwaitsecs longer than the job timeout so graceful shutdown works.
Overlaps. Long scheduled tasks stack up. Use withoutOverlapping() and onOneServer() on schedule entries, and cache locks inside jobs that must not run twice.
Restart on deploy. Add php artisan queue:restart to your deployment script. This is the single most common missing line in a broken Laravel deployment.
Timezone. schedule:run uses the app timezone. If your reports arrive at odd hours, check config/app.php before rewriting the job.
5. Make the failure loud next time
- Send
failed_jobsinserts to your error tracker, not just the database. - Alert on queue depth and oldest-job age, not just on worker liveness — a worker can be alive and useless.
- Run Horizon if you use Redis; its metrics and failure UI pay for themselves in the first incident.
- Add a scheduled heartbeat task that pings an external monitor. If the scheduler dies, the missing ping tells you within minutes.
- Prune
failed_jobson a schedule so the table stays readable.
When to bring someone in
If the backlog spans months, involves payments, or mixes idempotent and side-effecting work, the replay plan matters more than the code fix. That is the kind of work our Laravel application rescue sprint handles — one to two weeks, from $1,000, with the replay plan written down before anything is retried.
Related: fixing Stripe Cashier subscription and webhook failures, which is where broken queues most often show up as lost revenue.