Build1 distinct publisher3 min readUpdated
A reviewer of a dozen Next.js Stripe integrations says almost none guard against duplicate or out-of-order deliveries. The failure mode is a second welcome email and no error anywhere.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
A developer writing on dev.to says they have reviewed a dozen Next.js Stripe integrations, on client projects and in open source repos, and that almost every webhook handler was written as if duplicate and out-of-order delivery could not happen [2]. It matters because Stripe documents that both can: delivery is not guaranteed to be exactly once, and events are not guaranteed to arrive in the order they occurred, according to the post [1].
The handler in question is the one that shows up in roughly half of the Next.js Stripe tutorials online, by the author's estimate [3]. It verifies the signature, switches on `event.type`, and on `checkout.session.completed` sets the user's `subscriptionStatus` to active, sends a welcome email through Resend, and returns 200 [4]. Stripe retries a delivery automatically if the endpoint responds slowly, times out, or the server has a brief blip during a deploy [5]. On the retry, the same branch runs again: the status is set to active a second time, which is harmless, and the welcome email goes out twice, which is not [6]. Swap the email for granting credits, incrementing a usage counter, or inserting a row, and you get duplicated side effects silently, in production, with no error thrown to tell you [7].
The reason this survives code review is environmental. Forwarding events with the Stripe CLI locally produces exactly one delivery per event almost every time [8], and retries are triggered by precisely the conditions a laptop rarely reproduces: a slow response, a deploy at the wrong moment, a network hiccup [9].
The mitigation is unglamorous and cheap. Every Stripe event carries a unique `id`, so the handler checks whether that ID has been processed before doing anything else [10]. In the post's version, the route looks up a `ProcessedEvent` record by `event.id`, returns 200 immediately if one exists, otherwise runs the branch and then writes `{ eventId, processedAt }` [11]. A collection holding nothing but event IDs and timestamps is enough: the retry still gets a success response, so Stripe stops retrying, and no duplicate side effect occurs [12].
One thing to note about that sample: the guard row is written after the email is sent [11], so a crash between the send and the write leaves the guard unset and the next retry duplicates the email anyway [16]. If the side effect is expensive or externally visible, claim the event ID first and treat the insert itself as the lock.
Ordering is the subtler half, and it catches people who already fixed duplicates. Stripe does not guarantee that `checkout.session.completed` arrives before a later `customer.subscription.updated` for the same customer [13]. The common pattern updates the user by `stripeCustomerId`, a field the earlier event was supposed to set; if the later event lands first, the update matches nothing and disappears without an error [14]. The author's advice is to make each handler safe regardless of arrival order, for instance by resolving the user through a different key [15].
Worth checking on your own system: how many distinct event IDs your endpoint has acknowledged versus how many state transitions it actually performed. The gap, if any, is already in your outbox.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
Stripe does not guarantee that it will deliver a webhook event exactly once, and does not guarantee that events arrive in the order they happened; both are explicitly documented in Stripe's own docs.
The sample handler in app/api/webhooks/stripe/route.ts calls stripe.webhooks.constructEvent, switches on event.type, and for checkout.session.completed sets the user's subscriptionStatus to 'active' via User.findByIdAndUpdate, sends a welcome email with resend.emails.send, and returns a 200 'OK' response.
Stripe retries webhook delivery automatically if the endpoint responds slowly, times out, or the server has a brief blip during deployment.
When a retry arrives at the unguarded handler, the same logic runs again: the subscription status is set to active a second time, which is harmless on its own, but the welcome email is sent twice.
On handlers doing something less harmless, such as granting credits, incrementing a usage counter, or creating a database record, a duplicate delivery means duplicate side effects, silently, in production, with no error thrown anywhere.
Using the Stripe CLI locally to forward events yields exactly one delivery per event almost every time, a clean and predictable environment.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Single practitioner post; mechanism clear, external corroboration absent
One dev.to article by a self-described full-stack developer carries the entire cluster. The technical core is checkable in principle and internally consistent: the code blocks show exactly the pattern described, and the duplicate-email and silent-no-match conclusions follow from the code as written. But the two load-bearing platform assertions - no exactly-once delivery, no ordering guarantee - are stated as documented without any quotation or link, and the prevalence claim about a dozen reviewed integrations has no methodology, repository list, or counts. No second publisher, vendor statement, or postmortem corroborates anything.
No adoption signal in supplied sources
The cluster contains no release, deployment, benchmark, usage disclosure, pricing, or licensing event. The only quantitative gesture is the author's recollection of roughly a dozen reviewed integrations, which describes an absence of the guard rather than measurable uptake of any artifact, and no telemetry on real retry or out-of-order rates is offered. Adoption is therefore left unmeasured rather than inferred.
Sound mechanism, mildly overstated framing
The underlying engineering point is legitimate and the recommended guard is standard practice, so the gap is small rather than large. It is positive because the headline frames an unguarded webhook handler as a 'race condition' and generalises from an unaudited handful of codebases to 'almost none', and because the prescribed fix is presented as settled ('this alone fixes the majority of real-world duplicate delivery issues') while the sample itself leaves a residual window - the marker is written after the email send - and offers no protection against two retries racing the findOne/create pair.
Self-promotional developer post with paid-template funnel
The article closes with a link to the author's paid template store and a personal bio and social handle, and it ends with an engagement prompt asking readers to post their findings in the comments. Authority in the piece derives from the author's own unverifiable client-review experience, which is also the credential being marketed. There is no vendor sponsorship, affiliate disclosure, or competing-product disparagement visible, so the incentive is ordinary audience-building rather than a hidden commercial conflict.
Moderate on mechanism, low on scope
Confidence is moderate because the code-level reasoning is verifiable from the source itself and the remedy is a well-understood idempotency pattern, so the technical claims are unlikely to be wrong in kind. It is held down by the single-source cluster, the absence of any citation for the two Stripe platform guarantees, the absence of adoption or incidence data, and the promotional incentive attached to the author's prevalence claim.
build
A Stripe SDK Major Bump Turned One Metadata Lookup Into a Silent Non-Delivery1 distinct publisher
build
A $5-a-month monitoring SaaS on Workers, Turso and R2 is a cost datapoint, not a blueprint1 distinct publisher
build
AI built the store in weeks. Production itemised what the apprenticeship would have cost1 distinct publisher
product
PayPal stopped saying no. Payments teams should now plan for a Stripe-owned checkout rail3 distinct publishers
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 19, 2026