← Blog  ·  September 2026

Why Your Stripe Webhooks Are Failing Silently (and How to Know)

The payment succeeded on Friday. The order fulfillment webhook failed. You found out Wednesday — from an angry customer.

Webhooks are the nervous system of modern payment and integration flows. When a Stripe charge succeeds, a GitHub PR gets merged, or a Twilio message is delivered, a webhook fires and your backend processes the event. Usually this works fine. When it breaks, it breaks quietly — and the consequences can range from a confused customer to a missed refund to a permanently lost business event.

This article explains exactly how webhook delivery fails, what Stripe's retry behavior actually looks like, and what minimum monitoring you need to catch failures before your customers do.


How webhook delivery actually works (and where it fails)

When Stripe needs to notify your application — a charge succeeds, a subscription renews, a dispute is filed — it makes an HTTP POST to the endpoint URL you registered. Your server has a window (typically 30 seconds) to return a 2xx response. If it does, Stripe marks the delivery successful and moves on.

If your server returns a 4xx or 5xx, or times out, Stripe marks the delivery as failed and schedules a retry. The retry schedule is exponential:

After exhausting all retries (typically 15-17 attempts over 72 hours), Stripe marks the webhook event as failed and stops trying. It does not send you an alert. There is no email, no Slack notification, no dashboard popup. The event simply accumulates in the "Failed" section of your webhook dashboard in the Stripe console — visible only if you go look for it.

The dangerous part Stripe does have a "webhook alert" email feature, but it only fires after 3 failed deliveries over 24 hours to the same endpoint. By then you may have already missed events. And many teams configure the Stripe account under a billing email that nobody actually monitors.

Three real scenarios where silence costs you

Missed order fulfillment events

Your e-commerce backend listens to checkout.session.completed to trigger order creation, inventory decrement, and the confirmation email. Your webhook endpoint is down for 4 hours during a botched deployment. Stripe retries during the outage but your server keeps returning 503. By the time the server recovers, some events have been retried and succeeded — but others have accumulated multiple failures. Eventually those early events exhaust their retry window and are marked permanently failed. You have paid orders with no fulfillment action taken, and no way to know without manually cross-referencing the Stripe dashboard against your order database.

Failed refund callbacks

A customer requests a refund. Your backend receives the charge.refunded event and is supposed to update the order status and send a confirmation email. But the webhook endpoint throws a 500 because of a recently deployed bug in the refund handler. Stripe retries. The bug is still there. 72 hours later the event is marked failed. The customer receives no confirmation. Three days later they open a support ticket, then a dispute. You now have a chargeback on a refund you already processed — the worst of both worlds.

Lost GitHub Actions triggers

GitHub webhooks work the same way. Your CI/CD workflow fires on a push event delivered via webhook to your deployment pipeline. The pipeline receiver is temporarily unavailable. GitHub retries 3 times over a few minutes, then stops. The push event is lost. The deployment doesn't run. Your main branch is now ahead of what's deployed, and nothing in your dashboard indicates a deployment was skipped — because from your pipeline's perspective, no event ever arrived.


What "silent failure" actually looks like

The insidious thing about webhook failures is that from your application's perspective, nothing appears wrong. Your server is running. Your logs show no errors related to the events — because the events never arrived. The only place the failure is recorded is in Stripe's (or GitHub's or Twilio's) dashboard, under a section most developers check only when something is already broken.

If you have metrics on webhook event volume, a sudden drop is a signal. But most teams don't have per-event-type volume dashboards. They have uptime monitors that check if the endpoint returns 200 — which it may do perfectly well, just without receiving any traffic.


Minimum viable webhook monitoring

1. Enable Stripe webhook failure alerts (and point them somewhere real)

In your Stripe dashboard under Developers → Webhooks → select your endpoint → Alert emails. Add an email address that a human actually reads — not the billing address. This catches repeated failures but not the single-failure case.

2. Add an endpoint health check

Configure an uptime monitor (Uptime Robot, Better Uptime, or similar) to ping your webhook endpoint URL. This won't test that events are being received — it just confirms the endpoint is reachable. A down endpoint is the most common cause of webhook failures and the easiest to catch:

# A minimal health check endpoint in Express
app.get('/webhooks/health', (req, res) => res.sendStatus(200));

3. Log every received webhook with a timestamp

Every webhook handler should write a structured log entry on receipt: event type, event ID, timestamp, and initial processing status. This gives you a searchable record. If you suspect missed events, you can query your logs for the last checkout.session.completed event and compare against Stripe's event log for the same period.

4. Add a dead man's switch for critical event types

For your most critical event types (typically anything in the payment flow), implement a dead man's switch: if no event of type X has been received in Y hours, fire an alert. This is the only pattern that catches the case where your endpoint is healthy but Stripe has stopped sending events (due to earlier retry exhaustion on a now-forgotten failed attempt).

// Example: alert if no checkout.session.completed in 6 hours
// (assuming your store normally processes at least one payment per 6h)
const lastEvent = await db.query(
  `SELECT MAX(received_at) FROM webhook_events WHERE type = 'checkout.session.completed'`
);
const hoursAgo = (Date.now() - lastEvent.rows[0].max) / 3600000;
if (hoursAgo > 6) {
  await alertSlack('No checkout events received in 6+ hours');
}

🔔 Automated webhook monitoring

WebhookPulse monitors your webhook endpoints across Stripe, GitHub, Twilio, and more. Get alerted the moment deliveries start failing — before Stripe exhausts its retry window. Dead man's switches, endpoint health checks, and per-event-type volume monitoring included.

Join the WebhookPulse waitlist → Test a webhook endpoint

← All articles  ·  WebhookPulse  ·  Webhook tester tool