Skip to content

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 PENDING head waiting for retry blocks its successors;
  • a PROCESSING head under lease also blocks them;
  • DISPATCHED and DLQ are 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.