Webhooks & Events

The Outbox Pattern

The outbox pattern guarantees that an event is published exactly when its database change commits — never one without the other.

What you'll learn

  • Describe the dual-write problem the outbox solves
  • Explain how an outbox table ties events to a transaction
  • Outline a relay that delivers events from the outbox
  • Connect the pattern to reliable webhook publishing

7 min read

The dual-write problem

Imagine you update a record in your database and then send a webhook to announce the change. These are two entirely separate writes — one to your database, one out to the network — with no shared transaction binding them together. If the first succeeds and the second fails because of a crash in between, your data has changed but no event ever went out. Reverse the order and you risk the opposite: announcing a change that never actually committed.

This is the classic dual-write problem: keeping two independent systems consistent without a transaction that spans both. Naïve code that writes to the database and then publishes an event is quietly unreliable, and the failures it produces are intermittent enough to be genuinely hard to notice in testing and maddening to chase in production.

An outbox in the same transaction

The outbox pattern resolves this cleanly by writing the event into a dedicated outbox table within the same database transaction as the business change itself. Because they share one transaction, either both commit together or neither does. A recorded change therefore always has a matching pending event waiting beside it, and an event never exists for a change that did not commit.

BEGIN;
  UPDATE resource SET status = 'closed' WHERE id = 1;
  INSERT INTO outbox (type, payload)
    VALUES ('resource.updated', '{...}');
COMMIT;

The atomicity of that single transaction is the entire guarantee. There is no window in which the change and its event can diverge, because the database will not let one happen without the other.

Relaying events outward

A separate process, conventionally called the relay, reads the unsent rows out of the outbox table and delivers them — sending the webhook, publishing to a broker, whatever the destination is — then marks each row as sent once delivery succeeds. Because the relay works only from rows that are already durably committed, it can safely retry after a crash: an event simply stays in the outbox until its delivery finally succeeds.

This relay is naturally at-least-once by design, so a given row might be delivered more than once after a retry that overlapped a crash. That is exactly why consumers downstream must be idempotent, as set out in webhook idempotency. The pattern's real achievement is moving the entire reliability burden into a place you fully own and can reason about.

Why it matters for webhooks

For anyone in the business of sending webhooks, the outbox is the standard, battle-tested way to ensure that every meaningful change reliably produces a delivery. It decouples making the change from delivering the notification, which means a transient delivery failure never blocks, slows, or corrupts the underlying business operation. The user's action completes; the event catches up.

Even as a webhook consumer, the same underlying idea applies internally: persist the received event first, then process it asynchronously from an outbox-like queue you control. Combined with the delivery guarantees described in the API documentation, this approach yields a dependable, end-to-end pipeline that holds together even when individual pieces briefly fail.

Key takeaways

  • Writing to a database and the network separately is unreliable
  • The outbox writes the event in the same transaction as the change
  • A relay delivers outbox rows and retries safely after failures
  • Delivery is at-least-once, so consumers must be idempotent

FAQ

What problem does the outbox pattern solve?

The dual-write problem: keeping a database change and an event publication consistent. The outbox commits both atomically, so you never get one without the other.

Does the outbox guarantee exactly-once delivery?

No. The relay retries until it succeeds, so delivery is at-least-once. Consumers must be idempotent to handle the occasional duplicate that retrying produces.

Is the outbox only for sending webhooks?

It is most associated with publishing, but the same principle helps consumers too: persist the incoming event first, then process it from a durable queue.

Integrate with Merion

Ready to build?

Read the API reference, grab the OpenAPI spec, and ship a resilient integration.