No built-in poison message / dead-letter handling in EF Core Outbox
Contact Details
No response
Version
8.x (develop, pre-release)
On which operating system(s) are you experiencing the issue?
Windows
Using which broker(s) did you encounter the issue?
RabbitMQ
What are the steps required to reproduce the issue?
var provider = new DefaultServiceProviderFactory().CreateServiceProvider(services);
var mqConnectionString = provider.GetRequiredService<IOptions<RabbitMqConnection>>();
var masstransitConfiguration = provider.GetRequiredService<IOptions<MassTransitConfiguration>>();
var RabbitMQConfig = CapSettings.RabbitMQSettings.ExtractAmqpInformation(GetConnectionString(mqConnectionString.Value, false));
services.AddMassTransit(x =>
{
x.SetEndpointNameFormatter(new CustomEndpointNameFormatter());
// Register consumers (optional demo consumer)
x.AddConsumers(Assembly.GetEntryAssembly());
// Use the EF Core Outbox tied to AppDbContext
x.AddEntityFrameworkOutbox<UtilsDatabaseContext>(o =>
{
// Poll the outbox table for pending messages
o.QueryDelay = TimeSpan.FromSeconds(masstransitConfiguration.Value.QueryDelayInSeconds);
// Important: Use SQL Server-specific configuration
o.UseSqlServer();
// Use Bus Outbox to capture publishes within the consumer/handler transaction
o.UseBusOutbox();
o.DuplicateDetectionWindow = TimeSpan.FromHours(1);
});
// 3) This is the key: add a configure-endpoints callback
// applies to every endpoint created by ConfigureEndpoints(...)
x.AddConfigureEndpointsCallback((ctx, endpointName, e) =>
{
// Configure quorum queue arguments
if (e is IRabbitMqReceiveEndpointConfigurator rmq)
{
rmq.SetQueueArgument("x-queue-type", "quorum");
// Enable durable queues
rmq.Durable = true;
rmq.AutoDelete = false;
}
e.UseDelayedRedelivery(r =>
{
r.Interval(retryCount: masstransitConfiguration.Value.FailedRetryCount, interval: TimeSpan.FromSeconds(masstransitConfiguration.Value.FailedRetryIntervalInSeconds));
});
// Remove _error and _skipped queues
e.DiscardFaultedMessages();
e.DiscardSkippedMessages();
});
/// Audit Messages
services.AddScoped<IAuditStore, DomainEventAuditStore>();
//
services.AddSingleton<SendAuditObserver>();
// services.AddSingleton<ConsumeAuditObserver>();
// Configure RabbitMQ transport
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(RabbitMQConfig.HostName, RabbitMQConfig.VirtualHost, h =>
{
h.Username(RabbitMQConfig.UserName);
h.Password(RabbitMQConfig.Password);
});
// ✅ IMPORTANT: use the RabbitMQ delayed-exchange plugin for scheduling
cfg.UseDelayedMessageScheduler();
// Connect the configuration observer to apply filter to all consumers
cfg.ConnectConsumerConfigurationObserver(new GlobalFiltersConfigurationObserver());
// Configure endpoints for any consumers found
cfg.ConfigureEndpoints(context);
cfg.ConnectSendObserver(context.GetRequiredService<SendAuditObserver>());
// cfg.ConnectConsumeObserver(context.GetRequiredService<ConsumeAuditObserver>());
});
});What is the expected behavior?
Is there a planned or existing out-of-the-box mechanism to handle poison messages in the EF Outbox? For example: a configurable MaxDeliveryCount on outbox messages, a dead-letter table, a failure callback/hook in the delivery pipeline, or a way to mark individual messages as permanently failed without removing the entire OutboxState entry? Any guidance on the recommended approach for production systems facing this problem would be greatly appreciated.
What actually happened?
When using AddEntityFrameworkOutbox with SQL Server, the outbox delivery service polls OutboxState ordered by Created and processes messages sequentially to preserve ordering guarantees. This design introduces a critical gap: if a message permanently fails delivery — for example, due to payload size exceeding the broker's limit, schema mismatch, serialization errors, or any non-transient failure — there is no built-in mechanism to detect, skip, or dead-letter it after N failed attempts.
The result is an infinite blocking loop: the poison message sits at the front of the queue unlocked between retry attempts, gets re-fetched every polling cycle, fails again, and permanently blocks all valid messages behind it from ever being delivered
Related log output, including any exceptions
The message was not confirmed: PRECONDITION_FAILED - message size 32663502 is larger than configured max size 16777216Link to repository that demonstrates/reproduces the issue
No response
Source: MassTransit/MassTransit