# How do I handle poison-pill messages and a Dead Letter Queue in the queue?

> Put `tries` and a `backoff` on the job so it lands in `failed_jobs` at the cap, alert from a `JobFailed` listener and replay it with `queue:retry`.

- Asked: 2026-06-04
- Answered: 2026-06-07
- Asked by: Gökhan
- Tags: dayaniklilik, queue, laravel
- Source: https://www.muhammetsafak.com.tr/en/just-ask/poison-pill-messages-and-dead-letter-queues-in-laravel/
- Language: en-US
- Author: Muhammet Şafak

---
**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?


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.

1. **Bound the retries.** Put `tries` (or `maxExceptions`) and a `backoff` on the job. Now it's attempted 3-5 times, not infinitely. Once it hits the cap, Laravel stops re-queueing it.
2. **`failed_jobs` is your built-in DLQ.** When the cap is reached, Laravel moves the job to the `failed_jobs` table automatically — no extra infrastructure to build, that table *is* your Dead Letter Queue. Define a `failed()` method on the job and put your cleanup/compensation logic there.
3. **Wire up alerting.** Write a `JobFailed` listener that fires a Slack/Sentry notification the moment a job lands in `failed_jobs`. A silently failing job is the worst case; admins should know instantly. After inspection you replay the fixed job with `queue:retry`.
4. **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_jobs` once it crosses the threshold.
5. **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

- [Async Job Processing with Laravel Queue and Supervisor](/en/blog/async-job-processing-with-laravel-queue-and-supervisor/) — Blog
- [How do I design eventual consistency with the Saga Pattern in a distributed system?](https://www.muhammetsafak.com.tr/en/just-ask/orchestrated-saga-for-eventual-consistency-across-services/) — Just Ask
- [How do I set up a Circuit Breaker for a flaky third-party API dependency?](https://www.muhammetsafak.com.tr/en/just-ask/circuit-breaker-for-flaky-third-party-apis/) — Just Ask
- [Should I use chunkById instead of chunk when the same job also updates the rows it iterates?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-chunkbyid-instead-of-chunk-when-the-same-job/) — Just Ask
