HomeBlogStripe Webhooks in Next.js: The Failure Modes Nobody Warns You About

· 9 min read

Stripe Webhooks in Next.js: The Failure Modes Nobody Warns You About

Signature verification, idempotency, retries, out-of-order delivery, and the reconciliation bugs that only show up in production. A practical Stripe webhooks guide for Next.js.

Stripe webhooks look trivial: receive an event, update a row. Then you ship to production and discover the ways they quietly break — duplicate deliveries double-applying changes, signature checks failing on parsed bodies, events arriving out of order, and your database drifting out of sync with Stripe. This is the guide I wish more people read before wiring up payments. It draws on production payment work including the Dryva marketplace.

Failure mode 1: verifying the wrong body

Signature verification must run against the raw request body. In the Next.js App Router, that means reading await req.text() and passing it to constructEvent — never the parsed JSON. Any framework middleware that parses or re-serializes the body first will silently break the signature check, and you'll waste an afternoon before you realize the payload was mutated.

// app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
  const body = await req.text(); // RAW — do not JSON.parse first
  const sig = req.headers.get("stripe-signature")!;
  const event = stripe.webhooks.constructEvent(
    body, sig, process.env.STRIPE_WEBHOOK_SECRET!
  );
  // ...handle event
}

Failure mode 2: assuming exactly-once delivery

Stripe guarantees at-least-once delivery, which means the same event will arrive more than once. If your handler isn't idempotent, a retried checkout.session.completed can grant two months of access or trigger two payouts. Make writes idempotent: either record processed event.ids and skip duplicates, or model updates as upserts keyed by the subscription/charge ID so re-processing is a no-op.

// Idempotency guard
const seen = await db.processedEvents.find(event.id);
if (seen) return new Response("ok", { status: 200 });
await db.processedEvents.insert(event.id);
// ...apply the change (also safe as an upsert)

Failure mode 3: out-of-order events

Webhooks are not guaranteed to arrive in the order they occurred. A customer.subscription.updated can land before the created, or a cancel before a renewal. Don't blindly overwrite state from whichever event arrives last. Where it matters, check the object's current status from the event payload (or re-fetch from Stripe) rather than assuming sequence.

Failure mode 4: returning slow or throwing

  • Return 2xx fast. Do heavy work (emails, downstream calls) after acknowledging, or offload to a queue. Stripe treats a slow response as a failure and retries — compounding load.
  • Never let an unhandled event 500. Unknown event types should return 200, not crash. A thrown error triggers Stripe's retry storm.
  • Be explicit about which events you handle. Subscribe only to the events you act on; ignore the rest cleanly.

Failure mode 5: no reconciliation safety net

Even with perfect handlers, a deploy during an event burst or an endpoint outage can drop a change. Build a periodic reconciliation job that re-fetches recent Stripe objects and repairs any drift between Stripe and your database. Treat Stripe as the source of truth for money and reconcile your local state to it — don't assume webhooks alone keep you perfectly in sync.

Webhooks are an optimization for freshness, not a guarantee of consistency. The systems that don't lose money pair them with idempotency and reconciliation.

Testing before you trust it

Use the Stripe CLI (stripe listen --forward-to localhost:3000/api/webhooks/stripe and stripe trigger ...) to replay real event payloads locally, including duplicates and edge cases. Test the retry and duplicate paths deliberately — they're exactly the ones that bite in production. For deeper billing patterns, see my Stripe subscriptions on the App Router guide, and if you want this done right, I offer Stripe Connect integration as a service.

Related case studies

Building something like this?

I help startups ship AI agents, custom CRMs, React Native apps, and Next.js SaaS to production.

← All posts