Almost every provider says the same thing in its documentation: the same event can be delivered more than once. Stripe, GitHub and Shopify retry deliveries that time out or fail. Network problems mean a delivery can succeed on your side while the provider records a failure and sends it again.
If your handler creates an order, sends an email or credits an account for each delivery, a duplicate does it twice.
Reproduce it
hwcli replay evt_93df18ab --duplicate 3 --target :8080/webhooks
Then check the effect, not just the response codes: one order row, one email, one ledger entry. A handler that returns 200 three times and creates three orders has the bug.
What a correct handler does
- Records the provider's event ID (Stripe
id, GitHubX-GitHub-Delivery, ShopifyX-Shopify-Webhook-Id) with a unique constraint before doing the work. - Returns 2xx for an event it has already processed, so the provider stops retrying.
- Makes the work and the "processed" record one transaction, or makes the work itself idempotent.
Sequential duplicates don't catch race conditions, where two deliveries both check "not processed yet" before either records the event. For that, see Testing idempotency.
Keep it covered
Save the event as a regression test that expects the duplicate response your handler should give. That might be a 200 with {"duplicate": true}.