← All writing
Shipping & Ops·Sep 3, 2026·4 min read

Webhooks aren't idempotent until you make them

Every webhook provider says it may send the same event more than once. Almost every integration is written as if that line doesn't apply to them — until a customer gets charged twice.

H

Hammad Iqbal

Software Engineer

Stripe, GitHub, Twilio, and nearly every other webhook provider document the same guarantee: at-least-once delivery, which means duplicates are expected behavior, not an edge case. Most webhook handlers are still written to process each request as if it's guaranteed to arrive exactly once — because in testing, with one event and no network retries, it always does.

In brief
  • At-least-once delivery means your webhook handler will receive duplicate events; this is documented, not a bug on the provider's side.
  • Handlers that aren't idempotent double-process the second copy — sending a second email, granting a second credit, charging a second time.
  • The fix is a dedup check keyed on the event ID, done atomically before any side effect runs.

Why duplicates are normal, not rare

A webhook provider will resend an event if it doesn't get a fast 2xx response, if your endpoint times out, or sometimes for no visible reason at all as part of its own retry logic. None of that is a malfunction — it's the provider choosing to risk a duplicate over risking a dropped event, and it puts the responsibility for deduplication entirely on your handler.

The check has to happen before the side effect

A dedup check that runs after the email is sent or the charge is made doesn't help; the whole point is to stop the duplicate before it does anything observable.

  • Store the event ID in a unique-constrained table before doing anything else, and let the constraint violation short-circuit the handler
  • Do the dedup check and the side effect in the same transaction, not as two separate steps that can race
  • Return a 2xx for a duplicate you've already processed instead of an error — the provider will otherwise keep retrying

This is cheaper to build early than to patch later

Retrofitting idempotency after a duplicate has already caused a real, customer-facing problem means auditing every webhook handler in the codebase under pressure. Building the event-ID check into the handler template from the start costs one extra table and one extra query, and it means the second delivery of any event is a no-op instead of an incident.

Takeaway

If your webhook handler doesn't check the event ID before acting on it, you don't have a webhook integration — you have one that works until the provider retries.

Have a project this kind of thinking applies to?

Tell me what you're building — I read every message myself.

Available for new projects

4+

Years exp.

8+

Projects shipped

<6h

Avg. reply time

Let's build something

Tell me about your project.

Share what you're building, your timeline, and the best way to reach you.