Outbox + RabbitMQ Setup
Derived page. The behaviour described here is specified by the
outbox-and-messagingcapability underopenspec/specs/. That specification is the source; this page explains and illustrates it. Where the two disagree, the specification is right and this page is a bug.
Stratara.Outbox.RabbitMQ provides the IMessageBus implementation backed by a RabbitMQ broker. It uses publisher confirms + automatic reconnect + mandatory routing — failed-to-deliver messages are caught + retried from the outbox table.
Add the package
dotnet add package Stratara.Outbox.RabbitMQ
Configure
// appsettings.json
{
"RabbitMq": {
"HostName": "localhost",
"Port": 5672,
"VirtualHost": "/"
// Username + Password come from env vars in production:
// RABBITMQ_USERNAME, RABBITMQ_PASSWORD
// In Development only, the broker's default `guest/guest` is used.
}
}
Fail-fast outside Development (v3.4.0; v3.0.14+ for Production only): if RABBITMQ_USERNAME / RABBITMQ_PASSWORD are missing on any host that is not in Development, publishing throws InvalidOperationException naming the environment. The guest/guest fallback is Development-only — same pattern as the key-store guard.
Changed in 3.4.0. The check used to be
IsProduction(), which recognises exactly one name: Staging, QA, UAT, Preview, anything self-named, and evenProduction-EUandprodall fell through toguest. RabbitMQ restrictsguestto localhost by default, so a remote broker refused the connection anyway — but a broker in the same container or network running a default configuration accepted it. If you deliberately want the default account outside Development, setRABBITMQ_USERNAME=guestandRABBITMQ_PASSWORD=guestexplicitly; the configuration is the opt-in.
Wire the worker
A typical worker host wires both the outbox-drainer and the command consumer:
var builder = Host.CreateApplicationBuilder(args);
builder.AddOutboxWorkerServices(); // drains outbox_entry → publishes to bus
builder.AddCommandWorkerServices(); // subscribes to bus → dispatches commands
builder.Services.AddCommandHandlersFromAssemblyContaining<MyCommandMarker>();
await builder.Build().RunAsync();
Routing model
Topic and subscription names come from the Messaging configuration section. Every one has a
default, so a host that configures nothing still works:
| Topic | Default name | Default subscription(s) | Who publishes / consumes |
|---|---|---|---|
Command |
command |
command-subscription |
Your outbox (and other apps) publish; the command worker consumes |
HeavyCommand |
heavy-command |
heavy-command-subscription |
IHeavyCommand commands; the heavy-command worker consumes |
EventBundle |
event-bundle |
event-bundle-subscription, event-bundle-saga-subscription |
The write-store publishes; projection and saga workers consume |
Notification |
notifications |
— | Consumer-defined notification fan-out |
Override any of them by name:
{
"Messaging": {
"Topics": [
{
"Name": "Command",
"Value": "myapp.command",
"Subscriptions": [ { "Name": "CommandSubscription", "Value": "myapp.command.worker" } ]
}
]
}
}
Topics are fanout exchanges + per-subscription queues. Multiple worker hosts can scale out by sharing a queue — RabbitMQ does the work-stealing.
Backpressure
The OutboxWorker polls the outbox table every OutboxOptions.PollingIntervalSeconds (default 30) and publishes pending rows. If the broker is unreachable, rows sit in the table — at-least-once delivery preserved. The next poll-cycle retries.
A cycle takes one batch of each kind and ends. Rows the broker would not accept stay in the table and are retried on the next interval; a cycle never re-reads what it has just failed to publish. That bounds the work a cycle can do, and it is what stops an unreachable broker — or a suppressed drain during a projection replay — from turning a cycle into a loop over the same rows. The practical consequence: a large accumulated backlog drains at one batch per interval rather than in a single pass. With the defaults that is 20 000 rows a minute, and both knobs below are yours.
OutboxOptions.BatchSize (default 10_000) caps how many rows the worker claims per cycle, and LockLeaseSeconds (default 60) is how long a claimed batch stays leased to one worker. Bind them under the Outbox configuration section.
Connection health
Stratara.Outbox.RabbitMQ uses RabbitMQ.Client's automatic recovery + topology recovery. NetworkRecoveryInterval is set to a small default; consumers re-subscribe automatically after a reconnect.
On startup the bus fails fast in Production if the broker connection can't be established, rather than starting a worker that silently publishes nowhere.
Observability
The outbox plane records the outbox.published counter (on the Stratara.Service meter), tagged
by entry kind — command or event — so you can watch command-dispatch and event-bundle throughput
separately. It counts what the broker accepted, not what was read from the table: a row that
could not be published is not counted, so the counter going flat while the table stays full is the
signal that dispatch is stuck rather than busy. The Stratara.ServiceDefaults OpenTelemetry config wires the Stratara.Service meter
and the Stratara.Application activity source automatically.
Failure paths (PublishReturnException on no-binding, broker-disconnect, …) emit warning-level log events from Stratara.Shared.Diagnostics.Extensions.LoggerOutboxExtensions — see the LogEvents Schema.