Stripe Webhook Idempotency: Handle Duplicate Events Without Double-Charging

Stripe retries webhooks for up to 3 days. Without idempotency keys your handler will double-charge, double-provision, or create duplicate records. Here's the right pattern.

Why Stripe Retries Webhooks and for How Long

Stripe is designed for eventual consistency, meaning it’s okay if a webhook event isn't delivered immediately. To handle this, Stripe automatically retries sending webhooks up to three days after the original attempt. This retry mechanism makes your application more resilient to temporary network issues or service disruptions. Understanding this retry behavior is critical because it directly impacts how you design your webhook handler logic. A poorly designed handler can be repeatedly triggered, leading to unintended consequences like double charges.

The Idempotency Key Pattern

To prevent duplicate processing of webhooks, Stripe provides the idempotency key pattern. This is a crucial safeguard against accidental or malicious duplication. The core concept is simple: your webhook handler must verify that the event has not already been processed before taking any action. You achieve this by incorporating the `event.id` into a unique identifier and using the Stripe signature verification to confirm its origin.


// Node.js Example - Simplified
const stripe = require('stripe')('YOUR_STRIPE_SECRET_KEY');

async function handleWebhook(req, res) {
  const sig = req.headers['stripe-signature'];

  try {
    const eventData = await stripe.events.SignatureVerifier.parse(sig, req);

    // Create an idempotency key based on the event ID
    const idempotencyKey = eventData.id;

    // Verify that the event hasn't already been processed (implementation omitted for brevity)
    // ... your logic to check if the event has been processed using idempotencyKey

    console.log(`Webhook received for event: ${eventData.type}`);

  } catch (error) {
    console.error("Signature verification failed:", error);
    // Handle invalid signature - likely a malicious attempt
    res.status(400).send('Invalid webhook signature');
  }
}

Note that the actual implementation of checking idempotency depends on your application logic and persistence strategy (discussed below). The `eventData.id` provides a unique identifier for each event.

In-Memory vs. Database-Level Deduplication

A common initial approach to idempotency is in-memory deduplication – storing processed event IDs in a JavaScript object or similar data structure within your webhook handler. However, this is fundamentally flawed. If your webhook handler restarts due to an error, the in-memory store will be lost, and subsequent events that arrive during the restart will be incorrectly processed as new, leading to duplicate actions.

For reliable idempotency, you *must* use a persistent storage mechanism like a database (PostgreSQL, MySQL) or a distributed cache (Redis). This guarantees that event IDs are tracked across restarts and failures. Refer to our failed payment recovery guide for advanced strategies around handling events after processing.

Raw Body Requirement & Signature Verification

It's critical to understand that the raw webhook body *must* be a valid JSON object. Attempting to parse this JSON using `JSON.parse()` directly within your signature verification logic can break the process. The Stripe SignatureVerifier handles the parsing and validation of the headers, ensuring the data is in the expected format. Manually parsing or modifying the body before signature verification invalidates the signature, defeating the entire purpose.

The Processed Events Table Pattern (Postgres/MySQL)

A robust solution involves creating a table named `processed_events` in your database to track processed webhook events. This table should include columns such as: `event_id`, `created_at`, and any other relevant fields related to the event. You'll use this table to check if an event with the given `event_id` has already been processed before allowing your handler to act on it. This approach provides a reliable, scalable solution for managing idempotency across multiple deployments of your application. This is the most common and recommended method.

Common Mistake: Ignoring Signature Verification Before Deduplication

A frequent error is skipping the Stripe signature verification step entirely before attempting deduplication based on the event ID. While you’re using the `event_id` for checking, you *must* verify that it originated from Stripe to prevent an attacker from spoofing a webhook event and creating a duplicate record within your database. Always prioritize robust signature verification as the cornerstone of your idempotency strategy. Check out our Stripe decline code reference for further security considerations.

Is your webhook handler creating duplicate charges?

The free calculator estimates your monthly leak in 60 seconds. The $19 audit maps it to your real decline-code data.

Run the free calculator →

Free tool