Muhammad Tabarak - Home
Backend Engineering

Idempotent Stripe Webhook Handlers to Prevent Double Charges

Architecting atomic, idempotent Stripe webhook consumers in Node.js to eliminate double charges and handle network retries safely.

Muhammad Tabarak
Muhammad TabarakFounder & Software Engineer
November 3, 2026
3 min read
Idempotent Stripe Webhook Handlers to Prevent Double Charges

Payment webhooks are inherently asynchronous and untrusted. When Stripe processes a subscription payment or creator escrow release, their infrastructure promises at-least-once delivery. If your server takes more than 5,000ms to respond due to a slow database query, or if an intermediate load balancer drops the connection, Stripe retries the exact same event multiple times over several hours.

On financial platforms like Curved Lab, an unhandled duplicate webhook means crediting a creator's wallet twice or sending redundant platform notifications. Here is how we engineered an atomic, distributed idempotency layer that guarantees every financial event is processed exactly once.

The Fallacy of In-Memory Deduplication

Many systems attempt to guard against duplicate webhooks by storing processed event IDs in an in-memory Set or local cache. In real production setups running across multiple Node.js worker processes or serverless instances, this approach fails immediately. Two concurrent retries from Stripe hit different application processes simultaneously, both find the cache empty, and both credit the user's account.

A resilient idempotency architecture requires distributed locking combined with persistent, atomic state transitions in the primary database.

State Machine
Rendering interactive flow...
Figure: Distributed Idempotent Webhook State Machine

Eliminating Race Conditions with Atomic Distributed Locks

To prevent race conditions when two Stripe retries land within 20 milliseconds of each other, our webhook handler first attempts to acquire an atomic distributed lock in Redis keyed by the Stripe Event ID (lock:stripe:evt_xyz) with a 30-second TTL.

Only the single process that acquires the lock proceeds into the business logic. Any subsequent request attempting to process the same event is held in a polling backoff loop or immediately returns a 200 OK acknowledgment once the primary worker marks the event as completed in the persistent database.

Sequence Flow
Rendering interactive flow...
Figure: Parallel Retry Resolution with Redis Mutex & Deduplication

Production Checklist for Resilient Webhooks

  • Instant Signature Verification: Always verify the Stripe-Signature header using the raw binary body before attempting to parse JSON. Any mismatch terminates immediately with an HTTP 400.
  • Return 200 Fast, Process Async: For long-running operations like generating multi-page PDF receipts or heavy image generation, acknowledge the webhook with a 200 OK immediately after acquiring the lock and offload the actual job to a background worker.
  • Database Uniqueness Constraint: As a final safety net, the stripeEventId field in the database maintains a strict unique index. Even in the catastrophic event of a Redis outage, MongoDB will reject duplicate inserts with an E11000 duplicate key error.
Enjoyed this article?Add this site as a preferred source so Google surfaces my posts first — in Search & AI Mode.
Add as preferred source→