Webhook retry handling for payment events
Why it matters
Stripe retries failed webhook deliveries up to 3 days after the first failure. If your webhook handler uses plain db.create() or db.insert() rather than upserts, a retried event will either throw a unique constraint violation (silently dropping the event) or insert a duplicate row (corrupting subscription state). CWE-345 (Insufficient Verification of Data Authenticity) and ISO 25010 functional-correctness both apply. Handlers that perform slow synchronous operations (email sends, third-party API calls) before returning 200 also cause Stripe to time out and trigger additional retries, compounding the problem.
Severity rationale
Medium because non-idempotent handlers corrupt subscription state on retry and can leave billing records permanently inconsistent.
Remediation
Use upsert operations for all database writes in webhook handlers, and return 200 before performing any slow operations. Queue emails and third-party calls for background processing.
case 'customer.subscription.updated': {
const subscription = event.data.object
// upsert is safe to call multiple times with the same event
await db.user.upsert({
where: { stripe_customer_id: subscription.customer as string },
update: { subscription_status: subscription.status, plan: subscription.items.data[0]?.price.lookup_key ?? 'free' },
create: { /* ... */ }
})
// queue slow operations — don't block the 200 response
await queue.enqueue('subscription-changed-email', { customerId: subscription.customer })
return new Response('OK', { status: 200 }) // respond quickly
}
Alternatively, store processed event IDs in a processed_webhook_events table and skip re-processing if the event ID already exists.
DetectionPro
The detection method for this pattern is part of AuditBuffet Pro.
The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the Billing & Payments Audit. The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.
External references
- cwe · CWE-345 — Insufficient Verification of Data Authenticity
- iso-25010:2011 · functional-correctness — Functional Correctness — idempotent webhook processing ensures consistent billing state
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-billing·automated