Demo, all content is generated
Question

Lemon Squeezy webhook fires twice and I create duplicate orders

Solved · 157 views · asked by mcflyy · edited

Every now and then a customer gets two license keys because my webhook handler inserts the order twice. Looking at Lemon Squeezy's dashboard, the same event was sent twice a few seconds apart. Why would they send it twice and how do I stop it?

What I’ve tried

Bolt added a check "select order where email = x and created in last minute", which sometimes still lets both through.

Comment
How long does your handler take before it responds? nils_tw · edited

3 answers

Marked as helpful by the asker
nils_tw · edited

Webhook providers deliver at least once. If your endpoint is slow or doesn't return 2xx in time, they retry, so duplicates are normal and your handler must be idempotent.

  1. Make the database enforce it. Store the provider's order/event id with a unique constraint:
alter table orders add column ls_order_id text unique;

then insert with on conflict (ls_order_id) do nothing. Only issue the license key if the insert actually inserted a row.

  1. Return 200 quickly. Verify the signature, write the row, respond. Do slow work (emails, license generation) after.

Your "select then insert" check has a race: two requests can both select nothing at the same moment. The unique constraint can't race.

Comment
Very likely, yes. nils_tw · edited
makes sense. my handler was sending the email before responding which probably took long enough for the retry mcflyy · edited
jb_supa · edited

If the handler is a Supabase Edge Function: do the insert and the license assignment in one Postgres function called via rpc, so it's one transaction. Then the "order exists but no license" state can't happen either.

Comment
coop_builds · edited

From the Bolt side: tell it explicitly "the webhook handler must be idempotent, use a unique constraint on the provider order id and on conflict do nothing". With those words it writes it properly. "Prevent duplicates" gets you the select-then-insert version.

Comment