The event’s path, boundary by boundary
Event
invoice.paid
evt_7Q2M9KXD
sent by
billing-api
B1 · Accept
Gateway
Checks the sender and stamps the event with a receipt id.
mTLS + HMACschema v3p99 3 ms
If it failsBad signature or schema: 401 or 422 before a receipt exists. Nothing is stored.
B2 · Durable
Journal
Writes the event to a log that survives losing a machine, before anyone is told yes.
3 replicas, quorum 2fsync250 ms write timeout
If it failsNo ack. The sender retries with the same key and gets the same receipt, never a second event.
B3 · Route
Dispatcher
Reads the envelope and decides which services need the event.
routing rules v41per-consumer queuesat-least-once
If it failsA consumer is down: retries with backoff for 30 min, then dead-letters that one delivery.
B4 · Prove
Receipt
Collects every service’s acknowledgement as proof the event arrived.
closes at 3/3 acks5 min deadlineGET /v1/receipts/{id}
If it failsAn ack is lost: at 5 min the reconciler re-drives from the last acknowledged handoff.
Fig. 1The copper line is invoice.paid evt_7Q2M9KXD on its way through. Oxide lines are failures: at B1 and B3 the event stops and waits; at B2 and B4 it loops back to a point that is already safe.
Serif lines say what each part does. Mono lines carry the guarantees, timeouts and storage.
§ 02 Envelope
What the event carries
The Gateway wraps the sender’s payload in an envelope. Everything the Dispatcher needs to route is inside it, and after the Journal commit nothing inside it changes. Attempts and acknowledgements are written beside it, as delivery records keyed by receipt_id.
Envelope fixed after B2 commit
{"receipt_id": "rcp_4F81C2",B1assigns · the relay’s identity"event_id": "evt_7Q2M9KXD",senderalso the idempotency key"type": "invoice.paid",B3routes on this"schema": "invoice.paid/v3",B1validates"tenant": "acct_2291",B1from the credential · B3routes on this"source": "billing-api",B1from the mTLS identity"received_at": "2026-09-14T09:41:07.214Z",B1gateway clock"journal_pos": "p07:18442913",B2assigns at commit"payload_sha256": "9f1c04…e02a",B1computes · consumers can verify"payload": { "invoice": "in_5521", "amount": 48000, "currency": "EUR" }senderopaque to the relay}
Shaded: the two fields the Dispatcher routes on.
Delivery records change as work happens
| Consumer | State | Tries | Ack |
|---|---|---|---|
| ledger-writer | acked | 1 | +31.0 ms |
| analytics-sink | acked | 1 | +47.2 ms |
| email-receipts | acked | 1 | +64.1 ms |
§ 03 Timing
One event, timed: 64.6 ms from accept to receipt
Measured on the staging relay for evt_7Q2M9KXD. The sender has its 202 at 9.3 ms, once the Journal has committed and before any consumer has seen the event. Most of the remaining time is spent waiting on consumers, not inside the relay.
- +1.8 msB1Signature and schema checkedmTLS, HMAC, invoice.paid/v3
- +2.1 msB1Receipt rcp_4F81C2 assignedkey evt_7Q2M9KXD not seen before
- +8.9 msB2Two of three replicas confirm the writep07 · #18 442 913
- +9.3 msB1202 Accepted returned to billing-apithe sender’s part ends here
- +11.6 msB3Dispatcher reads the committed recordlog tail, in parallel with the 202
- +12.4 msB3Routed to three consumersrules v41 · type + tenant
- +31.0 msB4ledger-writer acknowledges
- +47.2 msB4analytics-sink acknowledges
- +64.1 msB4email-receipts acknowledgesslowest consumer sets the total
- +64.6 msB4Receipt closed, 3 of 3outcome delivered · visible at GET /v1/receipts
Times are from the Gateway receiving the request. Bars show the time each step took, on one linear scale; the dashed line marks the 202.
§ 04 Contract
The contract, stated plainly
No component needs to trust a later one. Identity and durability are both settled before the sender is told yes.
Identity is fixed once. The Gateway assigns
receipt_idand maps the sender’s idempotency key to it, so a retry can never create a second event.Nothing is acknowledged before it is durable. The 202 waits for two of three replicas. A sender with no answer retries; a sender with a 202 never needs to.
Everything after the Journal can safely run twice. Delivery is at-least-once and keyed by
receipt_idand consumer, so a re-drive sends only what is missing and consumers drop duplicates.
§ 05 Failures
What fails at each boundary, and what happens next
Each failure is handled at the boundary where it happens. None of them asks a later component to clean up.
| Boundary | What fails | What the sender sees | What the relay does | Where to look |
|---|---|---|---|---|
| B1Gateway | Signature, credential or schema check fails | 401 or 422 with an error code, and no receipt id | Rejects before a receipt exists. Nothing is stored, so nothing is retried. | gateway_rejects_total{reason} |
| B2Journal | Quorum not reached within 250 ms | 503 with Retry-After: 1 | Discards the partial write. A retry with the same key gets the same receipt_id. | journal_commit_seconds, journal_quorum_failures_total |
| B3Dispatcher | Consumer down, over 10 s, or answering 5xx | Nothing. It already has its 202. | Retries that consumer from 2 s, doubling to 5 min, for 30 min. Then dead-letters that delivery only. | dlq_depth{consumer} |
| B3Dispatcher | No routing rule matches the envelope | Nothing | Dead-letters at once with reason no_route. The fix is a routing rule, not a retry. | dlq_inflow{reason="no_route"} |
| B4Receipt | An ack is missing and no retry is scheduled, after 5 min | The receipt stays open | The reconciler re-drives the missing consumers from the Journal record. Consumers that acked are not sent it again. | receipts_open_age_seconds, reconciler log |
Open the failure pathThe same event, with email-receipts down
01 · B2 recordPreserve the event
The envelope stays at
p07:18442913, unchanged. Nothing is copied out of the Journal or rewritten, so the retry and the re-drive send the same bytes.02 · B3 deliveryRecord the boundary that rejected it
The email-receipts delivery record gets
boundary: B3, six attempts andlast_error: 503, then moves to dead-letter. ledger-writer and analytics-sink already acked and are left alone.03 · B4 receiptResume from the last acknowledged handoff
When email-receipts is back, a re-drive starts from the Journal record for that consumer only. The receipt closes 3 of 3 with outcome
delivered_after_redrive.
- B3email-receipts attempt 1 503
- B3attempt 2 503 · backoff 2 s
- B3attempt 3 503 · backoff 4 s
- …
- B3dead-lettered after 30 min, 6 attempts
- B4relay redrive rcp_4F81C2 --consumer email-receipts
- B4email-receipts acked
- B4receipt closed 3/3 · delivered_after_redrive
§ 06 Operating
Three signals to watch
Each one maps to a boundary. The dark bar is the normal range over the last 30 days; the oxide mark pages the relay on-call.
-
Commit latency, p99
How long a sender waits for its 202. Rises first when the Journal is unhealthy; pages after 5 min above 50 ms.
B2 · journal_commit_seconds{q="0.99"}warn 30 ms page 50 msthis event 6.8 · normal 7–12 ms60 ms
-
Oldest open receipt
How far proof is running behind delivery. Grows when handoffs are lost or a consumer stalls.
B4 · receipts_open_age_seconds{stat="max"}warn 5 min page 15 minnormal under 90 s20 min
-
Dead-letter inflow
Deliveries the relay has given up on, per consumer in a 15 min window. Any
B3 · dlq_inflow_total{consumer, reason}no_routeentry pages at once.warn 5 page 20normal 0–130 per 15 min
§ 07 Glossary
Words this note uses
- Envelope
- The relay’s wrapper around a payload: identity, type, tenant and a payload hash. Fixed once journaled.
- Receipt id
- The relay’s own id for an event,
rcp_…, assigned by the Gateway. Every later record points to it. - Idempotency key
- An id the sender chooses. Sending it twice returns the first receipt instead of making a second event.
- Quorum commit
- A Journal write counts once two of the three replicas have it on disk.
- Ack
- A consumer’s signal that it has processed the event. The receipt closes when every expected ack is in.
- At-least-once
- An event may be delivered more than once, never zero times. Consumers drop duplicates by receipt id.
- Dead-letter
- Where a delivery waits for a person after its retries run out, with the reason attached.
- Re-drive
- Sending an event again from its Journal record to the consumers that have not acked. The reconciler does it for lost handoffs.