How do I handle poison-pill messages and a Dead Letter Queue in the queue?
Question
On Laravel Queue (Redis), a job throws a Fatal Error every time it runs because of a bug in a third-party library, and it crashes the worker. Laravel auto-retries it and it crashes again; this poison message blocks the whole queue. How do I set up a workflow that separates these bad messages from the main queue, moves them to a Dead Letter Queue for inspection, and alerts the admins?
Answer
Short answer: a job that throws every time and is auto-retried forever locks the queue and crashes the worker — that’s a poison pill. The fix is to bound the retries and route the bad job to a DLQ instead of re-queueing it.
What you’re hitting is classic: the job blows up at the same spot, Laravel says “let me try again,” it blows up again, and the queue can’t move. You have to break the loop.
- Bound the retries. Put
tries(ormaxExceptions) and abackoffon the job. Now it’s attempted 3-5 times, not infinitely. Once it hits the cap, Laravel stops re-queueing it. failed_jobsis your built-in DLQ. When the cap is reached, Laravel moves the job to thefailed_jobstable automatically — no extra infrastructure to build, that table is your Dead Letter Queue. Define afailed()method on the job and put your cleanup/compensation logic there.- Wire up alerting. Write a
JobFailedlistener that fires a Slack/Sentry notification the moment a job lands infailed_jobs. A silently failing job is the worst case; admins should know instantly. After inspection you replay the fixed job withqueue:retry. - Handle Fatal Errors separately. Laravel’s normal retry doesn’t cover a Fatal Error that kills the worker outright (not an exception). Supervisor restarts the worker, but if the same job gets pulled again the loop never closes. For this, keep a “seen this job id N times” counter (in Redis) and force the job into
failed_jobsonce it crosses the threshold. - Separate queues by risk, make jobs idempotent. Run risky job types on their own queue so one bad job can’t starve all the others. And write jobs to be idempotent so retries are safe — running twice shouldn’t pile up side effects.
Bottom line: capped retries + the failed_jobs DLQ + alerting + replay via queue:retry. Never let any job retry unbounded; unbounded retry turns a single poison message into a weapon that halts the whole system.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.