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:
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.
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.