What is a dead letter queue (DLQ) — and how one broken message holds up the whole queue

You've built background processing: a user uploads a file, a job lands in a queue, a worker picks it up. If the worker crashes, the job comes back and it tries again. Reliable, right?
Now look. One user uploaded a file with broken encoding. The worker crashes on it. The job returns. The worker crashes again. Forever. Every attempt burns time, money on an AI call and space in your logs. And in some queues that message also sits at the head while everything behind it waits.
Such a message even has a name — a poison message. And the cure is called a dead letter queue.
What a DLQ is
A dead letter queue (DLQ) is a separate queue where messages go after they fail to process a few times.
The name comes from the post office. A letter with a bad address isn't thrown away or carried around forever. It goes to the "dead letter office", where a human sorts it out by hand.
The idea is exactly the same. The main queue keeps working. The problem message isn't lost — it sits apart, waiting for your attention.
How it works
The mechanics hang on one number — an attempt counter.
- A worker takes a message. The attempt counter goes up by one.
- Processing succeeds — the message is deleted.
- It fails — the message goes back into the queue for another try.
- The counter passes the limit — the message moves to the DLQ instead of back.
Where does "going back" to the queue even come from? Nearly every queue uses a visibility timeout. A worker takes a message — it isn't deleted, it's hidden for, say, 30 seconds. Processed and deleted in time — great. Crashed, hung, didn't delete — after 30 seconds the message is visible again and the next worker grabs it. So an "attempt" isn't necessarily an error in your code. It's any read after which the message wasn't deleted.
The practical takeaway: if processing takes longer than the visibility timeout, the message "comes back" while you're still working on it. A second worker takes it, the attempt counter goes up — and a perfectly healthy job ends up in the DLQ. Set the timeout with a margin above your slowest processing.
In Amazon SQS you set this up without code. A queue has a redrive policy with maxReceiveCount: how many times a message can be received before SQS moves it to the DLQ by itself. One detail from the docs: the DLQ must be the same type as the source. A FIFO queue gets only a FIFO DLQ.
Supabase Queues (pgmq under the hood) has no built-in DLQ. But every message has a read_ct field — how many times it's been read. That's enough to build a DLQ yourself:
-- the worker read message msg
if msg.read_ct > 5 then
insert into failed_jobs (payload, error) values (msg.message, last_error);
perform pgmq.archive('uploads', msg.msg_id); -- out of the queue, history kept
end if;
archive beats delete here: the message leaves the queue but stays in the archive. Later you can figure out what went wrong.
Why you need it
Without a DLQ you have two bad options.
- Retry forever. One broken message burns resources, clutters your logs and can stall the queue. If each attempt calls an AI model, that's real money too.
- Drop it after an error. Then you also lose good messages that failed by accident — a network blip or overload.
A DLQ gives you a third: a few attempts for random failures, then off to the side, for a human.
You'll meet this idea beyond queues. Services that send you webhooks work similarly: a few retries, then the event is marked undelivered. If your handler stays silent, check why a webhook isn't firing.
What to do with messages in the DLQ
A DLQ doesn't fix anything on its own. It's a box you have to look into.
- Set up an alert. A message landed in the DLQ — you get a notification. A DLQ without an alert is problems piling up quietly.
- Store the reason. Save the error text next to the message. A week later you won't remember why it failed.
- Fix and replay. Fixed the bug — send the messages from the DLQ back to the main queue. SQS has redrive for that.
How many attempts before the DLQ?
Usually 3–5. Enough to survive a network glitch, without wasting resources on a message that fails the same way every time.
Does a DLQ break message order?
Yes, it can. The SQS docs warn directly: if a FIFO queue needs strict ordering of operations, a DLQ will break it — one message moves away, the rest carry on.
Does a small project need a DLQ?
If you have background jobs with retries — yes, even just a failed_jobs table. Ten lines of code versus hours hunting for "vanished" jobs.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





