Webhook providers deliver on an at-least-once basis, which is a polite way of saying the same event will reach your endpoint more than once. The reasons are mundane: your handler took 5.1 seconds against a 5.0 second timeout, your 200 response was lost on the return path, or a retry landed on a second worker while the first was still working through the mechanics described by Prismatic. None of these are sender bugs.
The practical consequence is that the receiver carries the burden. Webhooks arrive as HTTP POST requests, and POST implies side effects, so idempotency has to be built rather than inherited as Hookdeck explains. A provider that retries is behaving correctly: dropping an event because your server hiccupped would be worse than sending it twice.
Retry windows vary enough that you cannot assume a fixed deduplication horizon. GitHub, for one, does not automatically redeliver failed deliveries at all; you re-drive them manually from the UI or API within a three-day window per GitHub's webhook best practices. Other providers retry on their own schedule. Design for the stingiest partner in your integration set, not the most generous one.
There is a second, quieter problem: ordering. Duplicates and out-of-order events travel together, and an old payment_intent.succeeded arriving after a newer refund can regress your state if you trust the payload. Fetch-before-process, where the webhook is treated as a notification and you refetch current state from the provider's API, sidesteps both issues at the cost of an extra round trip as Hookdeck's scale write-up describes.



