A3S Docs
A3S Event

Dead Letter Queue

Capture permanently failed events for inspection and debugging

Dead Letter Queue

Events that exceed the maximum delivery count are moved to a dead letter queue (DLQ) for inspection and debugging.

Enable DLQ

use a3s_event::{EventBus, MemoryProvider, MemoryDlqHandler};
use std::sync::Arc;

let mut bus = EventBus::new(MemoryProvider::default());

let dlq = MemoryDlqHandler::new(100); // Max 100 dead letters
bus.set_dlq_handler(Arc::new(dlq));

How It Works

When a message has been delivered max_deliver times without a successful ack, the event is routed to the DLQ handler:

Event delivered → processing fails → redelivered → fails again
    → max_deliver reached → DLQ handler receives the event

The max_deliver threshold is configured via SubscribeOptions:

use a3s_event::SubscribeOptions;

let options = SubscribeOptions {
    max_deliver: Some(3),  // After 3 failed deliveries, send to DLQ
    ..Default::default()
};

DeadLetterEvent

pub struct DeadLetterEvent {
    /// The original received event (includes delivery context)
    pub event: ReceivedEvent,

    /// Reason the event was sent to DLQ
    pub reason: String,

    /// Unix timestamp in milliseconds when the event was dead-lettered
    pub dead_lettered_at: u64,

    /// Original subject the event was published to
    pub original_subject: Option<String>,

    /// Number of delivery attempts before dead-lettering
    pub delivery_attempts: Option<u64>,

    /// Unix timestamp in milliseconds of the first delivery failure
    pub first_failure_at: Option<u64>,
}

Inspecting Dead Letters

let dlq = bus.dlq_handler().unwrap();

// Count dead letters
let count = dlq.count().await?;

// List recent dead letters
let letters = dlq.list(50).await?;
for letter in &letters {
    println!("[{}] {}: {}",
        letter.dead_lettered_at,
        letter.event.event.subject,
        letter.reason);
}

DlqHandler Trait

Implement custom DLQ backends (e.g., persist to database, forward to alerting):

use a3s_event::{DlqHandler, DeadLetterEvent};
use a3s_event::Result;
use async_trait::async_trait;

#[async_trait]
pub trait DlqHandler: Send + Sync {
    /// Handle a dead-lettered event
    async fn handle(&self, event: DeadLetterEvent) -> Result<()>;

    /// Get the number of events currently in the DLQ
    async fn count(&self) -> Result<usize>;

    /// List recent dead-lettered events
    async fn list(&self, limit: usize) -> Result<Vec<DeadLetterEvent>>;
}

Custom Example: Forward to Slack

struct SlackDlqHandler {
    webhook_url: String,
    memory: MemoryDlqHandler,
}

#[async_trait]
impl DlqHandler for SlackDlqHandler {
    async fn handle(&self, dead_letter: DeadLetterEvent) -> Result<()> {
        self.memory.handle(dead_letter.clone()).await?;

        reqwest::Client::new()
            .post(&self.webhook_url)
            .json(&serde_json::json!({
                "text": format!("DLQ: {} — {}",
                    dead_letter.event.event.subject,
                    dead_letter.reason)
            }))
            .send()
            .await
            .map_err(|e| EventError::Other(e.to_string()))?;

        Ok(())
    }

    async fn count(&self) -> Result<usize> {
        self.memory.count().await
    }

    async fn list(&self, limit: usize) -> Result<Vec<DeadLetterEvent>> {
        self.memory.list(limit).await
    }
}

MemoryDlqHandler

The built-in in-memory implementation with capacity management:

let dlq = MemoryDlqHandler::new(100);
// When capacity is reached, oldest dead letters are evicted

Default capacity is 1000:

let dlq = MemoryDlqHandler::default();

On this page