# Stripe event handlers tolerate out-of-order delivery by re-fetching current state or guarding on recency

- **Pattern:** `ab-002650` (`stripe-billing-integrity.event-processing.stripe-handlers-tolerate-out-of-order-events`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-10-05
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002650
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002650)

## Why it matters

Stripe does not guarantee that webhook events arrive in the order they were generated, and retries make reordering routine: a delayed `customer.subscription.updated` can land after the `customer.subscription.deleted` that logically followed it. A handler that writes `status`, `plan`, or period dates straight from `event.data.object` therefore lets a stale event overwrite newer state, so a customer who canceled appears active again, or a paying customer loses access because an older `past_due` update arrived last. The tempting fix, comparing `event.created` timestamps, is also unreliable: Stripe documents that it does not guarantee delivery order and `created` has one-second resolution, so two events in the same second tie. The robust patterns are to treat the event as a notification and re-fetch the object (`stripe.subscriptions.retrieve(id)`) before writing, or to guard the write with a monotonic marker stored alongside the row. This is a data-integrity failure rather than a security hole, but its effect is billing state that disagrees with Stripe, which turns into either free access or wrongly revoked paid access.

## Severity rationale

Medium because out-of-order overwrites cause wrong subscription state (a canceled customer shown as active, or the reverse) that persists until the next event, but they require specific delivery timing and are usually self-correcting on the next event.

## Remediation

Treat the webhook as a signal and read current state from Stripe before writing. For subscription events, call `stripe.subscriptions.retrieve(subscriptionId)` and write that object's `status`, `items`, and `current_period_end`, not the event payload's copy. If you must write from the event, store the Stripe object's own version marker alongside the row and refuse to apply an update that does not advance it. Do not order events by `event.created`. Re-fetching inside the request can push a handler past the Webhook Quick Response check, so persist the verified event, return 200, and re-fetch in the queued job.

```ts
case 'customer.subscription.updated':
case 'customer.subscription.deleted': {
  const sub = await stripe.subscriptions.retrieve((event.data.object as Stripe.Subscription).id)
  await db.subscription.update({ where: { stripeId: sub.id }, data: { status: sub.status } })
}
```

## Detection

**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 [Stripe Webhooks & Billing Integrity audit](https://auditbuffet.com/audits/stripe-billing-integrity). The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.

## External references

- external stripe-webhooks-event-ordering — https://docs.stripe.com/webhooks
- external stripe-event-object — https://docs.stripe.com/api/events/object

Taxons: data-integrity, error-resilience

HTML version: https://auditbuffet.com/patterns/ab-002650
