2026-09-27·by Sijie Wang#standmeet#architecture#design#events

message-loss-guarantees

Message loss: a guarantee per hop

Parent: events

Status: released in v0.1.76 (2026-09-27) — design and as-built record in docs/design/event-bus-outbox-webhooks.md in the StandMeet repo.

Every hop from write to delivery states what happens when it fails. The rule: at least once, and duplicates are absorbed by idempotency. What truly cannot be delivered ends in discarded, raises an alert and can be retried by hand. Nothing is lost silently.

The hops

Guarantee per hop

HopFailureGuarantee
① write → outboxThe transaction rolls backThe change and the event vanish together; "changed but no event" and "event but no change" are impossible
② outbox → jobThe relay crashes mid-batchClaim, enqueue and mark share one transaction; a crash rolls all back, and a restart claims again
② lost wake-upA NOTIFY is lost while the listener reconnectsThe 1-minute events relay sweep pokes the relay; the cost is latency, never an event
② a row that keeps failingThe relay fails on the same rowRetried row by row; after 5 failures the row is poisoned, set aside and raises events_poisoned; it is never deleted
③ job → executionThe worker is killed mid-jobA job stuck in running past the threshold is rescued and retried (River rescuer); a duplicate run is absorbed by idempotency
③ sustained failureThe receiver stays downBackoff to the limit → discarded → alert; the panel can retry by hand. Never silent.
④ received but droppedThe consumer returns 200 but fails to processOut of our control. The owner can inspect the stream with events.list. No scheduled reconciliation.
DuplicatesInherent in at-least-oncewebhook-id equals the event id and consumers dedupe on it; in-process subscribers are idempotent
MailSMTP 250 means accepted, not deliveredMessage-ID is <event id@standmeet>; bounce handling is out of scope
Owner-notify over the burst capMore than 5 access requests per owner per hourA deliberate, logged drop, so a flood does not mail the owner each time; every request stays visible in admin. A retry keeps its slot.
No mail supplierNothing can ever be sentThe job completes and logs; no alert
The database itselfDisk failureSame backups as business data (backup.sh); no extra promise

Hop ② once had a real loss bug: the first relay design read the outbox by a sequence cursor, and an interleaved commit could fall behind the cursor forever. The fix is to claim rows, not advance a cursor: relay-claims-rows-not-cursor.

Duplicates are the price, idempotency pays it

  • Webhooks carry the same webhook-id (the event id) on every retry. Consumers dedupe on it.
  • In-process subscribers must be idempotent. One registry-driven UT delivers the same event twice to every registered subscriber and asserts a single effect (no-bypass-by-structure).
  • Search indexing is an upsert and naturally idempotent.
  • Mail has no idempotency key in SMTP. The order is "send, then mark" (notified_at, replied, or the deleted booking notice), so a crash in between sends a duplicate. The message carries Message-ID <event id@standmeet>, and most mailboxes merge duplicates by it. That is the price of at-least-once, far better than the old "lost on failure".

How retries are scheduled and classified is in retry-has-one-owner.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →