Dead Letter Queues
A Dead Letter Queue (DLQ) captures messages that fail to be processed after a configurable number of attempts, preventing them from blocking the main queue.
Enabling DLQ
Enable DLQ when creating a queue by setting dlqEnabled and maxReceiveCount:
const queue = await rinda.queues.create({
name: 'order-events',
region: 'us-east-1',
dlqEnabled: true,
maxReceiveCount: 5, // Move to DLQ after 5 failed attempts
});
console.log('DLQ enabled:', queue.dlqEnabled); // true
This works for both standard and FIFO queues:
const fifoQueue = await rinda.queues.create({
name: 'payment-events',
region: 'us-east-1',
isFifo: true,
dlqEnabled: true,
maxReceiveCount: 3,
});
How It Works
Message sent → Queue → Consumer receives message
↓
Processing fails
↓
Message becomes visible again
↓
Retry (up to maxReceiveCount times)
↓
Moved to Dead Letter Queue
- A consumer receives a message and attempts to process it
- If the handler fails (throws an error), the message is not deleted
- After the visibility timeout, the message becomes visible again
- This repeats up to
maxReceiveCounttimes - After exceeding the retry limit, the message is moved to the DLQ
Worker with DLQ
The QueueWorker integrates naturally with DLQ. Failed messages are automatically retried:
const q = rinda.queue(queue.id);
const worker = q.createWorker({
handler: async (message) => {
const order = message.body;
if (!order.id) {
throw new Error('Invalid order — missing ID');
// Message becomes visible again after visibility timeout
// After maxReceiveCount failures, moves to DLQ
}
await processOrder(order);
return message; // Acknowledge
},
visibilityTimeout: 30,
});
worker.start();
Monitoring Dead Letters
Use message introspection to view dead-lettered messages:
const q = rinda.queue('QUEUE_ID');
const result = await q.inspect({
status: 'DEAD_LETTERED',
limit: 50,
});
for (const msg of result.data) {
console.log(`Failed message: ${msg.providerMessageId}`);
console.log(` Receive count: ${msg.receiveCount}`);
console.log(` Error: ${msg.errorMessage}`);
}
Redriving
Once you have fixed whatever made the messages fail, redrive them: they go back on the queue and are delivered again.
const q = rinda.queue('QUEUE_ID');
const result = await q.redrive();
console.log(`Moved ${result.moved} messages back onto the queue`);
Redriven messages get a fresh set of attempts. Without that reset each one would arrive carrying the delivery count that dead-lettered it, and the first failure after redriving would send it straight back — one attempt instead of the queue's full allowance.
Draining a large backlog
One call moves at most 500 messages and tells you whether there are more:
let moved = 0;
let result;
do {
result = await q.redrive();
moved += result.moved;
} while (result.hasMore);
console.log(`Redrove ${moved} messages`);
The cap is there so a single request cannot stay open for as long as a large backlog takes to move.
Moving a few at a time
Pass maxMessages to redrive part of the backlog — useful when you want to
watch the first few succeed before releasing the rest:
const result = await q.redrive({ maxMessages: 10 });
What happens if a redrive fails partway
A message is put back on the queue before it is removed from the dead-letter queue. If the second step fails, the message stays on the dead-letter queue and the next redrive moves it again — so a redrive can deliver a message twice, but cannot lose one.
Handlers should be idempotent for this reason, as they should be for any queue delivery.
Messages that could not be moved are reported separately and left where they were:
const result = await q.redrive();
if (result.failed > 0) {
console.warn(`${result.failed} could not be moved — they are still on the DLQ`);
}
Redriving from the dashboard
A queue with a dead-letter queue has a Redrive button on its overview page, showing how many messages are waiting. It drains the whole backlog, making as many calls as that takes.
Best Practices
- Always enable DLQ for production queues — prevents poison messages from blocking processing
- Set appropriate retry counts — 3-5 retries is typical for most workloads
- Monitor your DLQ — set up alerts when messages arrive in the DLQ
- Investigate root causes — use message introspection to understand why messages failed
- Redrive when fixed — once the issue is resolved, call
redrive()to put the messages back - Fix before you redrive — a message that fails again returns to the DLQ, so redriving into an unfixed bug just moves the backlog back and forth