Start here

Why webhook delivery is hard

Webhook delivery isn't just sending an HTTP request. You have to keep two facts in agreement — your business data changed and the webhook event was registered — even if the process crashes or rolls back in between. When those are two separate writes, that in-between moment is where systems break. This page walks the failure, what it costs, what existing services already solve, and the one seam they can leave open.

On this page:
Part 1The problem
Dual write

Two writes that can't be made atomic

A typical implementation does these in sequence. If they can't share one atomic transaction, there are two ways for it to break:

// Two separate steps — not one atomic unit:
await db.orders.insert(order);
await webhookProvider.send(event);
Pattern A — the lost webhook
Save the order to the DB (success)
   ↓
COMMIT the DB✗ Process dies / network fails
   ↓
The webhook event is never registered
  • The order exists.
  • The delivery layer never learned the event existed.
  • So it can't retry or redeliver — a delivery layer can't resend an event it never received.
Pattern B — the phantom webhook
Register the webhook event with the provider (success)✗ The business transaction fails
   ↓
The order data is rolled back
  • The order does not exist.
  • An external system is told an order was created.
  • The two systems now disagree about reality.
The same names are used on the home page: lost webhook (committed but never enqueued) and phantom webhook (enqueued but rolled back).
Cost

What it costs the business

An inconsistency rarely stays contained. Here's what actually happens — and the bill that follows:

  • An order is confirmed, but the warehouse is never notified, so it never ships.
  • A payment fails, yet an external service receives a success notification.
  • CRM, accounting, and inventory systems drift out of agreement.
  • The same event is processed twice — double billing or double shipment.
Direct loss
  • Double billing or refunds
  • Missed or duplicated shipments
  • Service that was never delivered
  • Engineer-hours spent on manual recovery
  • Data repair across external systems
Indirect loss
  • Erosion of customer trust
  • More support tickets to handle
  • Engineers pulled into incident triage
  • Gaps in audit trail and accountability
  • Planned work slips while you firefight
Recovery

Recovery needs people, not just a retry

Once an inconsistency exists, a simple resend is rarely enough. A human has to reconstruct what really happened:

  • Comb through application logs to reconstruct what happened.
  • Reconcile business rows in the DB against the delivery history.
  • Identify the events that were never sent, and resend them without double-sending.
  • Check and repair state on the external system's side.
  • Reach out to customers — refund, re-ship, or repair data.
The real problem isn't that one webhook failed. It's that after the incident, you no longer know which system holds the correct state.
Part 2The fix
Credit where due

What existing webhook services already solve

Modern webhook delivery services solve a great deal of the hard part — everything about delivering an event after it has been received. This is genuinely difficult work, and they do it well:

HTTP deliveryRetriesExponential backoffSigningDelivery historyDead-letter queue (DLQ)Endpoint managementMonitoringCustomer-facing dashboardsDelivery-result visibility
The last gap

The seam that can still remain

This is the same dual-write failure from the top of this page — only now the second write is registering the event with your webhook service. A delivery service can only retry events it has already received, so a gap remains whenever that registration is a separate step from committing the business transaction:

COMMIT to the business DB✗ The application stops
   ↓
Registering with the webhook service fails
   ↓
The delivery service never learned the event exists
  • A delivery service's retry only applies to events it received.
  • An event it never received appears in neither the delivery history nor the DLQ.
  • This is not a shortcoming of the webhook service — it is a seam left at the boundary between the application and the delivery layer.
This doesn't apply to every service or every setup. The inconsistency remains specifically when event registration happens outside the business transaction. We'll call this the transactional gap (or handoff gap).
The fix

How CommitCourier closes the gap

CommitCourier writes the business data update and the webhook outbox row in the same PostgreSQL transaction:

One PostgreSQL transaction
   ↓
Save business data + save webhook outbox row
   ↓
Commit together, or roll back together
This prevents the state where only one side succeeded — business data without an event, or an event without business data. CommitCourier doesn't claim to prevent every webhook failure; it structurally prevents the inconsistency between the business transaction and event registration.
And the delivery after that
Background deliverySigningRetries + backoffDLQ + ledgerReplaySSRF protectionEndpoint registryKey rotationOpenTelemetry

The full capability list lives on the home page; runnable code for each is on Integrate.

Part 3Using it
Two ways to use it

On its own, or alongside the platform you already run

CommitCourier can be your webhook delivery layer — or secure the handoff to the one you already use.

Pattern 1 — CommitCourier on its own
Your business logic
   ↓
CommitCourier
   ↓
Webhook receiver

With just PostgreSQL, you get enqueue, delivery, retries, DLQ, and history end to end — no extra infrastructure.

A good fit when you:
  • Don't run a webhook SaaS today
  • Want to manage delivery inside your own environment
  • Already operate PostgreSQL
  • Don't want to add another external service
Pattern 2 — alongside an existing webhook service
Your business logic
   ↓
CommitCourier
   ↓
Existing webhook delivery service
   ↓
Webhook receiver

Keep your webhook service as is, and use CommitCourier to close the handoff gap between the business transaction and the delivery layer. An experimental sink transport can even hand delivery off to that SaaS from the transactional outbox — so the handoff itself becomes reliable (see the FAQ).

A good fit when you:
  • Already run a webhook SaaS
  • Want the SaaS to keep handling portals and advanced monitoring
  • Don't want to restructure your setup
  • Only need to prevent the DB-to-provider inconsistency
Keep your webhook platform. Close the transactional gap.
Non-goals

What CommitCourier does not solve

To stay honest about scope: CommitCourier owns the consistency between the business transaction and event registration, plus delivery, tracking, and retries after that. It does not remove every webhook-related failure — receiver bugs, receiver-side idempotency, your own application logic, and PostgreSQL's own availability all stay yours.

The full responsibility boundary — what CommitCourier owns versus what you still own — is laid out on Built for safe adoption →.

Understand the problem, then watch it happen

You've seen the problem and the fix — now watch the failure and retry behavior play out in the live demo, then read the integration code.

Understand the problem
   ↓
Watch it run in the live demo
   ↓
Read the integration code
   ↓
GitHub / npm