Webhook delivery can be repeated. A provider may retry because it did not receive your response, even when your handler already saved the intended change. Your application needs a way to recognize the same logical event again.

Use the event identifier documented by the provider. Do not assume that two similar payload bodies are always the same event, or that a timestamp alone is unique.

Save identity and work together

A durable event table can record the provider, environment, event ID, receipt time, and processing state. Keep the unique constraint at the database boundary so two application replicas cannot both claim the event through a timing race.

Where possible, put the event claim and local state change in one transaction. Marking an event as processed before its work commits can lose an update. Doing the work before claiming the event can repeat it.

External side effects, such as sending another notification, need their own controlled delivery design. A local database transaction cannot automatically make every downstream system part of the same atomic operation.

Test duplicates under pressure

Deliver the same verified fixture twice, then send two copies concurrently to different handler instances. The resulting local state should match one logical update. Check side effects as well as the event-table row count.

Next, fail the handler after it claims the event but before the transaction completes. Your recovery path should allow the unfinished work to resume without falsely treating it as complete.

Keep duplicate detection records for a period that matches the provider's delivery and your recovery needs. A short-lived in-memory set may look correct in development but disappear at the exact restart when retries arrive.