September 2026 · 8 min read

Stripe webhooks fail silently — and most developers don't know until a customer complains

Every Stripe integration has the same Achilles heel: webhook delivery failures are silent. Stripe retries for 72 hours across 15 attempts, then stops — and it never sends you a push notification, email, or alert when every attempt fails. Your dashboard only shows you if you go looking.

The result: a customer paid, Stripe sent the checkout.session.completed event, your endpoint returned 500, Stripe retried for three days, gave up, and now that customer's account is stuck in "pending" state forever. You find out when they email support.

Why this is harder to catch than it looks

Webhook endpoints fail for reasons that are hard to predict and don't follow your deployment schedule:

The worst part None of these failures produce an alert. They produce silence. Your order fulfillment just stops.

What Stripe's actual retry schedule looks like

When your webhook endpoint returns a non-2xx status, Stripe queues retries on an exponential backoff schedule:

After 72 hours, the event is marked as failed. Stripe sends you a single "Your webhooks are failing" email during this window — to the account owner's email, not necessarily the developer, and often buried in Stripe summary digest emails. In practice, most teams don't see it until it's too late.

Real scenario

It's Friday evening. A deploy at 5pm breaks your webhook handler. Stripe starts retrying. By the time someone notices Monday morning — after a weekend of customer emails — 72+ hours have elapsed. Every event during that window is permanently failed. You have no way to replay them unless you built a replay mechanism yourself. You reconstruct orders manually from Stripe's event log.

The providers where this happens most often

Stripe gets the most attention, but every major provider has the same failure mode:

The pattern is the same everywhere: provider has exponential backoff, provider eventually gives up, your application state diverges from reality, you find out from users.

What a real detection system needs to do

The naive approach is checking Stripe's webhook logs manually. That works if you're paranoid and check daily. Most teams don't.

A proper webhook failure monitor needs to:

  1. Watch each provider's delivery log continuously — not just your server logs, which won't show you Stripe-side failures
  2. Alert on the first failure, not after 72 hours — the faster you know, the more events you can replay
  3. Track the full retry history — which attempts failed, with what status code, at what timestamp
  4. Provide one-click replay for recoverable events — after you fix your endpoint, you shouldn't have to manually re-send each event
  5. Work across providers — you probably have 3–5 webhook sources, not just Stripe

The code-level things you should already have in place

Before you need an external monitor, your webhook handler should be robust. The two most common implementation mistakes:

1. Doing synchronous work inside the handler

// Bad: if the DB insert takes >20s, Stripe marks it failed
app.post('/webhooks/stripe', async (req, res) => {
  const event = stripe.webhooks.constructEvent(...)
  await db.insert('orders', buildOrder(event))  // this blocks
  await sendConfirmationEmail(event)             // and this
  res.json({ received: true })
})

// Better: acknowledge immediately, process asynchronously
app.post('/webhooks/stripe', async (req, res) => {
  const event = stripe.webhooks.constructEvent(...)
  await queue.push({ event })  // enqueue for background processing
  res.json({ received: true }) // respond immediately
})

2. Not validating the webhook signature

Always verify stripe-signature against your webhook secret. Beyond security, this catches misconfigured secrets immediately instead of producing confusing downstream failures.

3. Not making handlers idempotent

Stripe can and does deliver the same event more than once (especially after network issues). Use the event ID as an idempotency key:

const alreadyProcessed = await db.findOne('processed_events', { id: event.id })
if (alreadyProcessed) return res.json({ received: true })

// ... process event ...
await db.insert('processed_events', { id: event.id, processedAt: new Date() })

What you actually need for ongoing monitoring

Even with a robust handler, failures happen. Infrastructure goes down. Dependencies have outages. Certificates expire.

The gap is passive monitoring — something that watches your webhook delivery health across all providers without you having to check dashboards daily.

I'm building exactly that: WebhookPulse monitors your webhook delivery across Stripe, GitHub, Twilio, Shopify, SendGrid, and more. When a delivery starts failing, you get alerted immediately — with the full retry history and one-click replay for recoverable events. $29/month flat.

Never find out from a customer that your webhooks are broken

WebhookPulse monitors your Stripe, GitHub, Twilio, and Shopify webhook delivery in real time — and alerts you at the first failure, not 72 hours later.

Join the early access waitlist →

Related: CrawlGuard — monitor AI crawler traffic spiking your infrastructure bill.