Outbox delivery and replay¶
The MIC outbox guarantees at-least-once delivery. It never promises exactly-once processing. Per-aggregate ordering is available, but must be enabled explicitly when a consumer cannot tolerate reordering.
Default throughput mode¶
Stores read pending events by priority and then arrival order. The dispatcher processes a batch sequentially and marks each event after publication. A failed event may therefore be overtaken by the next one.
With use_claim=True, a lease moves the event into processing. Expired leases
can be recovered after a worker dies, and SKIP LOCKED lets SQL workers claim
disjoint sets.
A crash after publication but before mark_dispatched() can create a
duplicate. Consumers must remain idempotent in every mode.
Per-aggregate ordering¶
Enable ordered mode on the dispatcher:
from mic.outbox import AsyncOutboxDispatcher
dispatcher = AsyncOutboxDispatcher(
store=store,
dispatch_fn=publish,
use_claim=True,
lease_ttl_seconds=60,
preserve_aggregate_order=True,
)
The ordering key is the (aggregate_type, aggregate_id) pair already carried
by OutboxEvent. In ordered mode, only the oldest non-terminal row of each
aggregate is visible:
- a
PENDINGhead waiting for retry blocks its successors; - a
PROCESSINGhead under lease also blocks them; DISPATCHEDandDLQare terminal and release the next row;- other aggregates continue in parallel.
Order is (created_at, id) within one aggregate. A higher priority on a newer
event cannot let it overtake its own head. With several workers, always combine
this mode with use_claim=True.
This guarantee creates neither global ordering across aggregates nor
exactly-once processing. On a large table, the application must also add and
measure a dialect-appropriate index on
(aggregate_type, aggregate_id, status, created_at, id).
Time-range replay¶
Replay remains separate from transactional dispatch. mic.event_replay reads
an immutable JSON or ClickHouse archive, filters by topic and inclusive time
range, and republishes in deterministic (created_at, original_id) order.
mic-events-replay --from-clickhouse http://clickhouse.svc:8123 \
--topic resource.changed \
--from 2026-09-29T08:00:00Z \
--to 2026-09-29T10:00:00Z \
--to-nats nats://nats-0:4222 --nats-stream events
Every bus publication carries x-replay: true, one x-replay-batch-id for the
batch, and x-original-event-id as the stable idempotency key. Without a sink,
the command performs a dry run. A publication error stops the batch, so a retry
may redeliver its prefix.
Operational choice¶
| Need | Primitive |
|---|---|
| Reliable publication after a transaction | OutboxDispatcher + OutboxStore |
| Concurrent dispatch with crash recovery | use_claim=True + leases |
| Strict ordering within each aggregate | preserve_aggregate_order=True + claim |
| Rebuild from history | mic.event_replay + archive |
| Exactly-once or global ordering | Outside MIC guarantees; domain idempotency and topology required |
See also Retry without idempotency.