← All notes
Notes

Designing for the webhook that arrives twice

Aug 2026 5 min read

Payment webhooks will eventually duplicate. Not because a provider's code is broken, but because the honest contract of most webhook systems is "at least once," not "exactly once." Building ResuMaxxing's billing layer around that assumption up front turned out to matter more than any single feature in it.

The setup

ResuMaxxing's subscription tiers are driven entirely by Lemon Squeezy webhooks — order_created, subscription updates, cancellations. Each one has to update a user's access level in the database. The naive version of this is simple: receive event, verify it's really from Lemon Squeezy, update the row. That's correct exactly until the same event arrives twice, which webhook providers do on purpose — timeouts, retries, and at-least-once delivery guarantees mean duplicates aren't an edge case, they're part of the spec.

Designing around it

Two things had to be true before any event was trusted, and one thing had to be true after.

First, every incoming request is verified with a constant-time HMAC SHA-256 signature check against the raw payload. Constant-time matters here specifically — a naive string comparison leaks timing information that, in theory, lets an attacker guess a valid signature one byte at a time. It's a small detail that costs nothing to get right and is genuinely bad to get wrong.

Second, the handler itself is idempotent: applying the same event twice has to leave the database in the same state as applying it once. In practice that means keying off the event's own ID rather than just its payload, so a replayed order_created doesn't double-grant a subscription tier or double-log activity.

Third, the actual database write happens through a non-blocking background worker rather than inline in the request handler. Lemon Squeezy expects a fast response to the webhook itself — it's not the place to also be doing a synchronous write under load. Decoupling "acknowledge the event" from "apply the event" keeps the webhook endpoint fast regardless of what's happening on the database side.

What I'd tell someone else

If you're integrating any payment webhook, don't treat "handle duplicate events" as a hardening pass you'll get to later. Design the write to be idempotent from the first line of the handler, because retrofitting idempotency after a subscription tier has already been double-granted in production is a much worse afternoon than building it in on day one.

← What six months of on-call taught me Back to all notes →