Menu

Our webhook dedupe check was a race, not a guard
📰

Our webhook dedupe check was a race, not a guard

DEV Community: backend·Payneteasy·about 8 hours ago
#LY9BCYRr
#devtobackend#devto#dev#event#payment
Reading 0:00
15s threshold

We deduplicated webhooks the obvious way. Handler receives an event, runs SELECT on processed_events by event_id, and if nothing is found, does the work and inserts the row at the end.

It held for months. Then a customer got two receipt emails and a loyalty-points credit twice for the same payment.

The cause: the gateway redelivered the event 40 ms after the first attempt, because our first response was slow (a cold DB connection pushed it to about 3 seconds, and their timeout was 2). Both deliveries landed on different pods. Both ran the SELECT before either had inserted. Both saw an empty table, and both did the work.

In logs it looked like duplicate delivery, but the gateway was behaving correctly. Our check-then-act was the bug.

The fix was to flip the order. Insert the event_id first, into a table with a unique constraint, inside the same transaction as the side effects. If the insert hits a conflict, return 200 and stop.…

Continue reading — create a free account

Join HashtagPLUS to read full articles, follow hashtags, vote, and join the conversation.