Skip to content

Dead-letter and poison cap

By default a failing message is redelivered forever and a nack(requeue = false) discards it. Two opt-in settings bound that: dead_letter(key) copies dropped and poison messages to the named key (same transport family, stream to stream or list to list) instead of discarding them, and max_deliveries(n) caps the delivery count.

The copy is tagged with the x-dead-letter-reason header (dropped or max-deliveries) and written before the original is acked, so a crash leaves a duplicate rather than a loss.

// Cap redeliveries at 5. On the 5th failed delivery (or an explicit drop) the message is copied to
// the "orders.dlq" stream, tagged with the reason, rather than retried forever or discarded.
#[subscriber(
    RedisStream::new("orders")
        .group("workers")
        .dead_letter("orders.dlq")
        .max_deliveries(5)
)]
async fn handle_order(order: &Order) -> HandlerResult {
    if order.id == 0 {
        // A poison message: nack to retry. Once the cap is reached it is dead-lettered for you.
        return HandlerResult::Nack { requeue: true };
    }
    println!("processed order {}", order.id);
    HandlerResult::Ack
}
// Cap redeliveries at 5. On the 5th failed delivery (or an explicit drop) the job is LPUSH-ed to
// the "jobs.failed" list, tagged with the reason, rather than retried forever or discarded.
#[subscriber(
    RedisList::new("jobs.dlq")
        .reliable()
        .dead_letter("jobs.failed")
        .max_deliveries(5)
)]
async fn handle_job(job: &Job) -> HandlerResult {
    if job.id == 0 {
        // A poison job: nack to retry. Once the cap is reached it is dead-lettered for you.
        return HandlerResult::Nack { requeue: true };
    }
    println!("processed job {}", job.id);
    HandlerResult::Ack
}

max_deliveries(n) caps the delivery count. It is checked against both the framework retry-count header (the nack/republish loop) and, on the Streams reclaim path, the native Redis Streams delivery count, so a message poisoning either way is caught. Reclaimed deliveries also carry redis-delivery-count and redis-idle-ms headers, so a handler can branch or dead-letter manually.

Simple List and Pub/Sub cannot ack, so they have no dead-letter path.