← Back to blog

Your app has two Stripe customers for the same user, and neither of them notices

September 5, 2026

Stripe will let you create as many Customer objects as you want for the same email address. It doesn’t deduplicate, doesn’t warn you, doesn’t even surface it anywhere prominent in the dashboard. That’s reasonable behavior on Stripe’s side — plenty of real businesses legitimately want multiple customer records tied to one email, like a parent company with several subsidiaries. But if your app only expects one customer per user and quietly ends up with two, you get a specific, ugly failure mode: a real, paying subscription sitting on a Stripe customer object your app has completely stopped looking at.

The three ways this actually happens

1. A retried request during signup

Your signup flow calls customers.create, the request times out or the connection drops before the response comes back, and your code — reasonably, defensively — retries it. Except the first request usually did go through; Stripe processed it, just didn’t manage to tell you in time. The retry creates a second customer with the same email. Your database saves whichever customer ID came back last, and the first one becomes an orphan that still technically exists and can still be charged, refunded, or subscribed — just not by anything your app is watching.

2. Guest checkout, then account creation

A visitor checks out as a guest before creating an account — Stripe Checkout happily creates a customer object for them on the spot. Minutes later they sign up properly, and your onboarding code creates a second customer object because it has no idea the first one exists; nothing about the signup form knows to go looking for a guest checkout that happened under the same email ten minutes earlier. The subscription they just paid for lives on customer A. Their new account links to customer B. Their account page shows no active plan.

3. A webhook that races your own onboarding logic

Some integrations create the Stripe customer from a webhook (say, on user creation in your auth provider) and from a direct API call somewhere else in the codebase — a support tool, an admin backfill script, a mobile app hitting a different endpoint than the web app. Whichever path runs second has no way of knowing the first one already ran, because there’s no natural place that checks first. Two code paths, two customer objects, one user.

Two-minute check: Stripe Dashboard → Customers → search by a real user’s email address. If more than one result comes back, you have this bug in production right now. Do it for three or four of your actual paying customers, not just one — a single lucky search coming back clean doesn’t rule it out.

Why your own logs won’t show you this

Nothing here throws an error. Stripe’s API returns a perfectly valid 200 with a perfectly valid customer object every time — it has no way of knowing you didn’t want a new one. Your webhook handler processes customer.subscription.created correctly, for the correct (second) customer. Your database write succeeds. Every individual step in the chain does exactly what it was told to do. The bug only exists in the gap between two customer objects that both look completely legitimate on their own, and nothing in a typical stack is watching for that gap, because it isn’t an error condition from any single system’s point of view.

What it costs when a real customer hits it

This is one of the few billing bugs that a customer discovers themselves, immediately, instead of you catching it in a report weeks later. They pay, get redirected back to your app, and see a paywall or an “upgrade to Pro” prompt on a plan they just bought. The instinct is to assume they got scammed, not that there’s a data-modeling bug three layers down — and the support ticket that follows reads exactly like that. Multiply this by however many people sign up through a guest checkout flow or hit a flaky network during onboarding, and it stops being a one-off and starts being a small, steady leak of trust that never shows up as a single dramatic incident, just a slow trickle of angry first-day emails.

The fix

The first three prevent new duplicates. The fourth is the one that finds the customers who are already stuck wrong today, sitting quietly in your Stripe account with an active subscription your app has never once checked.

What we built this for

I built Venwai after running into a version of this exact problem. It connects to Stripe with a read-only key, polls a single endpoint you expose on your own app once a day, and diffs “who Stripe says is paying” against “who your app says has access” — including cases like this one, where the mismatch traces back to a duplicate customer object rather than a missed webhook. The moment the two disagree, it alerts you in Slack, Discord, Telegram, or email, instead of waiting for the customer to find it first.

Free while it’s in beta, no card required: venwai.com.