Skip to main content
Version: Next

Dead-Letter Deployment

The dead-letter feature can be deployed in two topologies: same-process (relay runs inside the publisher application) or cross-process (dedicated worker process).

Same-Process Deployment

Register all components in the same application host:

services.AddEventPublisher(options =>
{
options.Source = new Uri("https://orders.example.com");
options.ThrowOnErrors = false;
})
.AddRabbitMq(options => { /* transport config */ })
.AddDeadLetter()
.WithEntityFramework(options => options.UseSqlServer(connectionString))
.WithReplayWorker(options =>
{
options.Interval = TimeSpan.FromSeconds(30);
options.MaxRetryCount = 3;
});

Pros

BenefitDescription
Simple setupOne deployment, one configuration
No additional infrastructureNo separate worker process to manage
Good for developmentEasy to test and debug

Cons

DrawbackDescription
Resource contentionRelay competes with main application for CPU/memory
Coupled lifecycleRelay restarts when application restarts
Cannot scale independentlyMust scale entire application to scale replay

When to Use

  • Development and testing
  • Low to medium message volumes
  • Applications where simplicity is paramount

Cross-Process Deployment

Split the feature across two applications:

Publisher Application

Publishes to the real transport and persists failures into the dead-letter store:

// Publisher application Program.cs
services.AddEventPublisher(options =>
{
options.Source = new Uri("https://orders.example.com");
options.ThrowOnErrors = false;
})
.AddRabbitMq(options => { /* transport config */ })
.AddDeadLetter()
.WithEntityFramework(options => options.UseSqlServer(connectionString));
// No WithReplayWorker() here

Worker Application

Connects to the same store and runs WithReplayWorker():

// Worker application Program.cs
services.AddEventPublisher("transport", builder => builder
.Configure(options =>
{
options.Source = new Uri("https://orders.example.com/transport");
options.ThrowOnErrors = true;
})
.AddRabbitMq(options => { /* transport config */ }));

services.AddEventPublisher()
.AddDeadLetter()
.UseRepository<DbDeadLetterMessage, EntityDeadLetterMessageStore<DbDeadLetterMessage>>()
.WithFactory<DbDeadLetterMessage, DefaultDeadLetterMessageFactory<DbDeadLetterMessage>>()
.WithReplayWorker(options =>
{
options.TransportPublisherName = "transport";
options.Interval = TimeSpan.FromSeconds(30);
});

Pros

BenefitDescription
Independent scalingScale workers without scaling the publisher
Isolated failuresWorker crashes don't affect the publisher
Dedicated resourcesWorker can run on different hardware
Simplified deploymentUpdate worker logic without redeploying publisher

Cons

DrawbackDescription
More complex deploymentSeparate worker process to manage
Shared databaseRequires database access from both applications
Additional infrastructureNeed to monitor and maintain worker

When to Use

  • High message volumes
  • Mission-critical applications requiring high availability
  • When replay processing is resource-intensive
  • When you need to scale replay independently

Comparison Table

AspectSame-ProcessCross-Process
Setup complexitySimpleModerate
InfrastructureMinimalAdditional worker process
ScalabilityLimitedExcellent
IsolationNoneFull
Resource contentionYesNo
DeploymentSingleMultiple
Best forDev/Test, low volumeProduction, high volume