Tell Me When Down
How it worksWhat we checkPricing
BlogFree toolsCompareDocs
Log inGet started
All posts
Stripe webhooks you can trust

Stripe webhook duplicate events: stop the double charge

July 18, 2026·6 min read
Dice on a board in low light — the same event landing more than once, each doing the work again.

A customer gets charged twice for the same thing. Your code only meant to charge them once. The bug isn't in your charge logic — it's that Stripe delivered the same webhook event twice, and your handler did the work both times.

Duplicate events aren't a Stripe malfunction; they're a guarantee you have to design around. Once you understand why they happen, making your handler immune is a small, permanent fix.

Why the same event arrives twice

Stripe's webhook delivery is at-least-once, not exactly-once. That's a deliberate reliability choice: Stripe would rather send an event one extra time than risk you never getting it. Two normal situations produce a duplicate:

at-least-once
Stripe's delivery guarantee — duplicates are expected
retries
a slow 2xx can arrive after Stripe already retried
evt_...
the event id — stable across every redelivery
24h
how long Stripe honors your own idempotency keys
  1. Retry after a slow response. Your handler does the work but takes too long to return 2xx. Stripe times out, assumes failure, and retries — so the work runs a second time even though the first one actually succeeded.
  2. Genuine redelivery. Stripe's infrastructure occasionally sends the same event more than once on its own. You're promised at least one delivery, never exactly one.
The dangerous version isn't a duplicated log line — it's a duplicated side effect: a second charge, a second provisioned seat, a second "welcome" email, a credit applied twice. Anything your handler does to the outside world can happen twice unless you stop it.

The fix: make the handler idempotent

Idempotent means running it twice has the same effect as running it once. The standard way to get there uses the one thing that stays constant across every redelivery: the event id.

Every event carries a stable evt_ id. Before doing any work, record that id in a processed_events table with a unique constraint. If the insert succeeds, it's the first time you've seen this event — do the work. If it fails on the unique constraint, you've already handled it — acknowledge with 2xx and stop.

Insert first, process second. That ordering also handles the race where two copies arrive at the same instant: the database lets exactly one insert win, and the loser bails out cleanly.

paste into Claude or ChatGPT
Make my Stripe webhook handler idempotent so a duplicate delivery can't double-charge or double-provision. I'm using <FRAMEWORK> and <DATABASE>.

Requirements:
- Stripe can deliver the same event more than once (at-least-once delivery), and retries after a failed 2xx also re-send events. My handler must be safe to run twice with the same event.
- Use the Stripe event id (evt_...) as an idempotency key: before doing any work, try to record the event id in a "processed_events" table with a unique constraint. If it's already there, acknowledge with 2xx and do nothing.
- Make the record-and-process step safe against races (two deliveries arriving at once) — e.g. insert-first with the unique constraint, or a transaction, so only one wins.
- For any outbound side effects I create myself (like creating a charge or sending an email), show me where to attach my own idempotency key too.
- Return 2xx once the event is safely recorded as processed.

Give me the full handler plus the SQL for the processed_events table.

Your own idempotency keys, too

There's a second layer worth knowing. When your code calls the Stripe API to create a charge or subscription, you can attach an Idempotency-Key header. Stripe honors it for 24 hours: send the same key twice and the second call returns the original result instead of creating a duplicate. Use a key derived from the event you're processing, and even a duplicated handler run can't create two charges.

Stripe's idempotent requests docs cover the header in detail.

The two failure modes are opposites — watch for both

Duplicate events double things up; the other half of the problem is events that never arrive at all, leaving a paid customer un-provisioned. That's the silent-failure side of the same system. Both are invisible from your app until money or trust is already lost.

Tell Me When Down watches the endpoint your billing depends on, so a webhook that starts erroring — and triggering the retries that cause duplicates in the first place — reaches you before your customers' card statements do.

Charge once, provision once — and know when the endpoint's in trouble.

Join Tell Me When Down free and we'll watch your Stripe webhook endpoint around the clock. When it starts erroring — the exact condition that triggers retries and duplicates — you get an email in minutes, not a double-charge complaint.

Watch my webhookfree · no card required
more on stripe webhooks you can trust
Stripe webhooks failing silently — and how to catch itThe payment succeeds even when the webhook fails, so nothing looks broken until a customer writes in. The real retry-and-disable timeline, the raw-body trap, and how to catch it inside the window.Stripe webhook signature verification failed: the raw-body fix"No signatures found matching the expected signature" almost always means a body parser changed the request bytes before verification ran. Here's what Stripe actually compares, and the raw-body fix for each framework.Stripe subscription not activating after paymentPaid but still not upgraded? The subscription's status splits it in two: stuck in 'incomplete' means the payment never finalized; active-in-Stripe-but-not-your-app means a webhook never landed. Here's how to tell which.How to test Stripe webhooks locally (and what it won't prove)The Stripe CLI brings events to your machine — stripe listen forwards them, stripe trigger fires them. Here are the commands, the signing-secret gotcha, and why passing locally doesn't prove the live endpoint works.

spot something wrong or out of date? [email protected] — we'll fix it

Tell Me When Down

Uptime and security monitoring for people who'd rather ship than babysit servers. We watch so you can sleep.

product
How it worksWhat we checkPricingDocsBlogFAQ
free toolsWebsite security scanSupabase pause checkRender sleep checkMixed content checkerSecurity headers checkCookie security checkSSL expiry check
comparevs UptimeRobotvs Better Stackvs PingdomFor indie hackers
company
StatusAbout our botSupportPrivacyTerms
© 2026 TellMeWhenDown · tellmewhendown.com