LR-06

Stripe Will Retry Until It Breaks You.

Stripe delivers webhooks at least once. Not exactly once. At least once.

When your server takes too long to respond, returns an error, or drops the connection, Stripe retries the event. Retries escalate from seconds to minutes to hours — a typical pattern extends over a window that can last days. If your handler was slow rather than broken, the original request and the retry can arrive within seconds of each other — and both execute.

If your handler isn't idempotent, each retry triggers the same side effects again. A subscription activates twice. An order fulfills twice. Credits are granted twice. A customer may be charged twice — or may see duplicate fulfillment and then file a chargeback. Either way, you don't find out until the support ticket or dispute arrives.

This is not a rare edge case. Stripe's own documentation explicitly warns about at-least-once delivery and the need for idempotent handling. And in AI-generated webhook handlers, idempotency is almost never implemented.


Who This Is For

  • Founders whose app processes Stripe webhook events to activate subscriptions, fulfill orders, or grant credits
  • Developers who built webhook handlers with AI tools and aren't sure if duplicate events are handled
  • Teams that experienced chargebacks or customer complaints about double billing
  • Anyone whose webhook handler performs side effects (sending emails, provisioning resources, updating records) without checking if the event was already processed

If your webhook handler runs the same logic every time an event arrives — without checking whether it's already been processed — your billing system will eventually produce duplicates.


What Founders Experience

  • It starts with a support ticket. A customer writes: "I was charged twice." Or: "I received two confirmation emails." The founder checks Stripe and sees one payment. But the app processed the event twice.
  • The customer doesn't wait. In one documented case, a SaaS founder described: "Billing bug. Charged them $99 on the 1st and again on the 3rd. Instead of emailing me, they did a chargeback." The customer didn't complain — they went straight to their bank.
  • Chargebacks cost more than the transaction. Stripe charges a $15 non-refundable dispute fee when a chargeback is filed. If you contest and lose, it's another $15. For a $9.99 subscription, contesting a chargeback costs more than three months of revenue.
  • The spiral compounds. Double charges lead to chargebacks. Chargebacks increase your dispute rate. A dispute rate above 0.9% (Visa, since January 2026) triggers account review. Account review can lead to holds, restrictions, or termination. One documented case: Stripe froze $40,000 of a client's revenue after the chargeback rate crossed the threshold.
  • The root cause is invisible. The handler looks correct. It processes events. It activates subscriptions. It works perfectly when events arrive once. The problem only manifests under retry conditions — which happen silently, without alerts, in production.

What's Actually Happening

Stripe's webhook delivery is designed for reliability, not uniqueness. The same event can be delivered multiple times by design.

Three conditions trigger retries:

  1. Your handler returns a 5xx error. Any server error triggers a retry.
  2. Your handler takes longer than 30 seconds. Stripe considers this a timeout, even if the first handler is still running. The retry arrives while the original request is still being processed.
  3. The connection drops. Network issues, load balancer timeouts, or cold starts can prevent Stripe from receiving your response.

Two failure modes make this dangerous:

1. No Event ID Tracking (The Common Case)

The handler receives the event, processes it, and returns 200. It does not record which event IDs it has already processed. When Stripe retries the same event, the handler processes it again — activating the subscription again, granting credits again, sending the confirmation email again.

AI-generated webhook handlers almost never include event ID tracking. The AI generates the happy path: receive event → process → respond. Idempotency is a production concern that doesn't surface in development testing.

2. Check-Then-Write Race Condition (The Subtle Case)

The handler checks if the event was already processed, then writes the result. But under concurrent requests, both the original and the retry check at the same time, both see "not processed," and both proceed. The check passes twice because the write hasn't completed yet.

This is particularly common when the handler takes more than a few seconds — long enough for a 30-second timeout retry to arrive while the first handler is still running. Both instances pass the idempotency check because neither has finished writing yet.


What This Puts at Risk

Customer trust. A double charge — even if quickly refunded — damages the customer's confidence in your billing. Research suggests billing errors create 3.2x higher churn in the following 12 months, even after the issue is resolved.

Direct financial cost. Each chargeback costs $15–$30 in Stripe fees alone, regardless of whether you win the dispute. Double-charged customers who file chargebacks cost you the transaction amount plus fees.

Stripe account risk. Visa's chargeback threshold dropped to 0.9% in January 2026. Crossing this threshold triggers automated review, which can result in fund holds, processing restrictions, or account termination. One documented case: $40,000 frozen with no warning. Another: $130,000 held for over 8 months.

Support burden. Every duplicate charge generates a support ticket that requires manual investigation: checking Stripe logs, identifying the duplicate, issuing a refund, and communicating with the customer. At scale, this consumes significant founder time.


How Trust Score Detects It

BIL-04: Idempotent webhook processing. Checks whether your webhook handler has protection against processing the same event multiple times. This includes detecting event ID tracking patterns, database-level uniqueness constraints on event identifiers, or other deduplication mechanisms.

This check exists because Stripe's at-least-once delivery guarantee means duplicates are not a bug — they are a design feature. Your handler must be prepared for them.


Real Incidents

Double-charge during high-traffic sale (August 2025). A SaaS founder described on LinkedIn: "Our backend was not idempotent, so multiple POST requests to the order API resulted in duplicate orders and charges." The issue was discovered through customer support tickets during a sale event, not through monitoring.

$40,000 frozen after chargeback threshold (January 2026). A founder wrote on LinkedIn: "Stripe froze $40,000 of my client's revenue on a Tuesday morning. They couldn't pay their supplier. They couldn't run ads. They couldn't even pay themselves." The freeze was triggered by a 1.4% chargeback rate — partly driven by billing inconsistencies from duplicate processing.

$99 double-charge, customer filed chargeback directly (November 2025). A Reddit r/SaaS post: "Billing bug. Charged them $99 on the 1st and again on the 3rd. Instead of emailing me, they did a chargeback." The customer bypassed support entirely.

Webhook debug incident — verification disabled, duplicates processed. A team disabled signature verification during debugging. The webhook URL was exposed, and fake events compounded with real retries. Three fake payment events were processed within hours, each triggering fulfillment.

Silent billing failures (industry data). Research suggests that billing failures and silent processing mismatches — through duplicates, missed events, or processing errors — can add up to meaningful revenue leakage. At $10K MRR, even a small percentage of silent failures compounds to thousands of dollars per year before any catastrophic event.


Detection: How to Check Your Own App

Check 1: Does your handler track processed events?

Search for event ID tracking in your webhook handler:

# Search for event ID deduplication patterns
grep -r "event.id\|event_id\|idempotency\|already.*processed\|duplicate" --include="*.ts" --include="*.tsx" --include="*.js" | grep -i "webhook\|stripe"

Interpretation: If your webhook handler doesn't reference the event ID or check for duplicates, every retry will trigger full reprocessing.

Check 2: Side-effect smoke test

Use the Stripe CLI to trigger the same event type twice in quick succession:

# Trigger a test event
stripe trigger checkout.session.completed

# Wait a few seconds, then trigger the same event type again
stripe trigger checkout.session.completed

# Check: did your app create two subscription records?
# Check: did the customer receive two confirmation emails?

Interpretation: This does not recreate the exact same event ID retry path — each CLI trigger generates a new event. But it reveals whether your handler safely deduplicates repeated webhook-triggered side effects. True idempotency should be based on event.id uniqueness, not just event type.

Check 3: Check for uniqueness constraints

If you store processed events in a database, verify that there's a uniqueness constraint:

-- Check for unique constraint on event tracking
SELECT constraint_name, table_name
FROM information_schema.table_constraints
WHERE constraint_type = 'UNIQUE'
AND (table_name LIKE '%event%' OR table_name LIKE '%webhook%');

Interpretation: Without a database-level uniqueness constraint on the event ID, application-level checks are vulnerable to race conditions.


Related Launch Risks


FAQ

Stripe says "at least once." Does that really mean events arrive more than once?

Yes. Stripe's documentation explicitly states that webhooks use at-least-once delivery. Retries happen when your server is slow (over 30 seconds), returns an error, or drops the connection. During high traffic or cold starts, this is common. Your handler must be designed to handle the same event arriving multiple times.

Our handler checks the database before processing. Isn't that enough?

Only if the check and the write are atomic. If two requests arrive within milliseconds of each other — which happens when a 30-second timeout triggers a retry while the original is still running — both can pass the "not yet processed" check before either has written the result. A database-level uniqueness constraint on the event ID is more reliable than an application-level check-then-write.

How often do duplicates actually happen in production?

It depends on your handler's response time and your infrastructure's reliability. Cold starts (serverless functions), slow database queries, and external API calls in webhook handlers all increase the probability. During traffic spikes and sale events — exactly when duplicates are most damaging — the likelihood increases.

We've been running for months with no complaints. Are we safe?

Possibly not. Not all customers complain — some file chargebacks directly. Some don't notice small duplicate charges. And some duplicates affect internal state (like credits or access levels) without generating a visible customer-facing problem. The absence of complaints is not evidence of correct handling.

What's the simplest way to add idempotency?

Store each processed event ID in your database with a uniqueness constraint. Before processing, attempt to insert the event ID. If it already exists (unique constraint violation), skip processing and return 200. This handles both retries and race conditions at the database level.


Is This Happening in Your App?

Run a free Trust Score scan — 24 safety checks across auth, billing, and admin. Results in seconds.