What the outbox table itself needs to store, and why it is not a generic event log
standardintermediateThe outbox table has a narrower, more specific job than a generic event log — it exists purely to hold events that are waiting to be relayed, not to be a permanent historical record. It needs enough structure for the relay to reliably pick up, publish, and then retire each row: an identifier for the event, enough context to route or serialize it correctly (what kind of event it is, what business entity it relates to), the actual payload, and a way to track whether it has been published yet. Once an event is confirmed published, the outbox row has done its job — unlike an event log, which is often kept indefinitely as a source of truth, an outbox table is a transient staging area that is safe to prune once the relay has finished with a row.
Think of it as
An outbox table is like an outgoing mail tray on someone's desk, not a filing cabinet of every letter they have ever sent. The tray only needs enough information to get each letter mailed correctly — an address, the contents, maybe a note about which folder it relates to — and once the mail carrier has actually picked it up, there is no reason to keep that letter sitting in the tray any longer. A filing cabinet (an event log), by contrast, is meant to be a permanent archive people can search through indefinitely. Both hold similar-looking pieces of paper, but they exist for genuinely different purposes, and treating the outgoing tray as if it were the permanent archive means it never gets emptied and keeps growing forever.
What we're doing: Show an outbox table left unbounded because it was treated as a permanent log, and the cleanup fix.
- 8
- This is the accumulation — every one of these rows did its job successfully and then was simply never removed.
- 13
- This is the real cost — a table designed to be small and fast for one narrow query now pays a growing tax for holding data it no longer has any use for.
Why this works: This is the concrete consequence of treating an outbox table as if it were a permanent event log — the publish logic itself can be entirely correct while the table still degrades, purely because retention was never designed as part of the table's lifecycle.
Deleting a row immediately after calling publish(), before confirming it actually succeeded
Wrong
Better
What you see: An event is silently lost — the outbox row was deleted the instant publish() was called, but the broker never actually received it (a network failure right after the call), and with the row already gone, there is no remaining record that the event was ever supposed to be sent.
Why: Deleting immediately after calling publish() conflates "we attempted to send it" with "it was actually confirmed delivered" — the same distinction the relay concept in this section already makes for marking rows published; cleanup needs to be a separate, later step that only touches rows already confirmed and past a safe retention window, never an immediate action tied to the publish attempt itself.
- Outbox table
- Transient staging until relayed
- Deleted/archived once published
- Queried for almost nothing after publish
- General event log
- Durable historical record
- Kept indefinitely, or per long-term policy
- Queried for auditing, replay, analytics
A typical outbox row shape, and outbox vs a general event log
Remember: An outbox table needs just enough structure to relay events reliably — an ID, entity/event type, payload, and a published flag — and is a transient staging area, not a permanent event log. It needs its own retention/cleanup policy for rows already confirmed published, or a table meant to stay small keeps growing indefinitely for no remaining purpose.
See also: outbox relay implementation · idempotency implementation

