/ blog/stripe-webhooks-idempotency
blog / stripe-webhooks-idempotency / overview.md

Handling Stripe Webhooks Resiliently with Idempotency Keys

Webhooks fail. Webhooks retry. Webhooks arrive out of order. If your payment architecture isn't idempotent, you will inevitably double-charge users or double-provision resources.

The Reality of Webhooks

When Stripe processes a payment, it sends an invoice.payment_succeeded webhook to your server.

Your server receives it, provisions the user's premium account, but right before responding 200 OK, your database hangs and the request times out.

Stripe didn't get the 200 OK, so it retries the webhook an hour later. Your server provisions the premium account again. If provisioning involves sending welcome emails or crediting API tokens, the user just got double.

Idempotency is Mandatory

An idempotent operation is one that produces the same result no matter how many times it is executed.

Every Stripe webhook payload includes an id. This is your idempotency key.

The Implementation

app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);

  // 1. Check if we have processed this event ID
  const processed = await db.processed_events.findUnique({
    where: { event_id: event.id }
  });

  if (processed) {
    // We already handled this! Just return 200 to satisfy Stripe.
    return res.status(200).send();
  }

  // 2. Process the business logic
  await handleStripeEvent(event);

  // 3. Record the event as processed
  await db.processed_events.create({
    data: { event_id: event.id, type: event.type }
  });

  res.status(200).send();
});

Taking it Further: Database Transactions

The snippet above has a race condition. If two identical webhooks arrive at the exact same millisecond, they might both pass the findUnique check before either has time to write to processed_events.

To fix this, use Postgres UNIQUE constraints on event_id. Try to insert the event first. If it throws a unique constraint violation (Error 23505), you know it's a duplicate, and you can safely ignore it.

Tags

backendarchitecturestripe
0
0