Outbox
Commit a job with your business data in a database that is not the job store, then let a relay deliver it at least once.
On this page
Staging only works when your business tables and Runwick's tables share one database. When they do not (your data is in one PostgreSQL database and the jobs in another, or the job store is Redis), no single transaction can span both. The outbox closes that gap:
- Your transaction writes your rows and an outbox message to your own database. They commit together.
- A relay reads committed messages and delivers each one to the Runwick store.
- The store remembers each delivery, so a message the relay sends twice still creates one job, within a time window.
Packages
The source is your business database. The destination is the Runwick store the host is configured with: PostgreSQL, SQLite or Redis. Redis can be a destination but not a source.
Set it up
Install the source schema
Run this once per source database, with a role that may create schemas. It creates a
runwick_outboxschema (PostgreSQL) orrunwick_outbox_*tables (SQLite) and returns the source's ID. It is safe to run again.Text For SQLite:
await SqliteOutboxSchema.InstallAsync("Data Source=/var/lib/my-app/app.db");(namespaceRunwick.Outbox.Sqlite).Register Runwick and the outbox
Text AddRunwickPostgresOutboxregistersIPostgresOutboxand the relay, which runs as a hosted service.AddRunwickSqliteOutboxdoes the same for SQLite. Only one source adapter can be registered per host; a second registration throws.Stage messages in your transaction
Text
StageAsync writes inside a savepoint on your transaction. It never contacts the destination, and never commits, rolls back or retries your transaction. If it rejects the message, your transaction is left as it was. It returns a StagedOutboxMessage with the message ID, the proposed job ID and AcceptUntil, the end of the de-duplication window. Nothing is delivered until you commit.
Staging options
Processes that only stage
A process that stages but should not deliver can turn the relay off: relay: o => o.Enabled = false. It still needs AddRunwick with the destination store, because staging checks the destination. Before the runtime has connected to the store once, staging fails with outbox_destination_unknown. To stage before that, set the destination store's ID: destination: new OutboxDestination { StoreId = storeId } (the ID that the migration returned).
Relay options
Bad values throw when you register the outbox. The relay opens at most two connections to the source. Several relays can serve one source: each message is leased to one relay at a time. The relay also deletes delivered rows from the source in small batches.
The relay needs a destination store that accepts outbox deliveries; all three Runwick stores do. If the store does not, the relay fails when it starts.
The de-duplication window
Each message carries AcceptUntil: the time it was staged plus its acceptance horizon (7 days by default). Up to that moment, a message the relay delivers twice, after a lost reply or a crash, maps to the same job. The destination keeps that mapping until at least 24 hours after AcceptUntil.
After AcceptUntil, the destination accepts nothing new for that message. If it was delivered in time, the relay finds the mapping and marks it delivered. If not, the message is blocked with horizon_passed_undelivered, so a person can decide. Restaging the same ID returns the original cutoff, never a longer one. To try again, stage a new message ID and accept that the work could happen twice.
Blocked messages
The relay blocks a message, rather than retrying it forever, when it cannot be delivered as written. Examples: the job type is no longer registered (contract_not_registered), the message was bound to another store or application (destination_binding_mismatch), the window passed (horizon_passed_undelivered), or the store rejected it.
Blocked messages are never rewritten or retried automatically. They stay in the source table with state blocked and a blocked_reason (in PostgreSQL, runwick_outbox.outbox_messages). Fix the cause and reset the row to pending, or accept the loss. A destination that is simply down is not a reason to block: the relay retries with backoff.
What the outbox does not promise
- Not exactly-once. Delivery is at-least-once with de-duplication inside the window. A delivered message means the store accepted it; the job itself is still at-least-once like any other.
- No ordering. Messages are not delivered in the order they were staged, even from one source.
- No de-duplication after the window, after the destination's mappings are cleaned up, or after the destination is restored from a backup without them.
- No safe source restore beyond the destination's retained window.
- SQLite sources are same-machine only. Every process that uses the SQLite file must run on one machine.
Errors when staging
None of these writes anything. A database error such as a deadlock or SQLITE_BUSY reaches you unchanged; retry the whole transaction.