Meta Description: Stop processing duplicate webhook events in n8n workflows. Learn the simple idempotency pattern that prevents double-charging customers when Stripe retries failed webhooks.

Target Keyword: n8n webhook idempotency

Draft Created: 2026-03-03 Source: n8n community post (topic 272743), production webhook best practices Confidence: High (7/10) - Common pain point, active community discussion today


Outline

Introduction (100 words)

  • The nightmare scenario: customer gets charged twice because your webhook handler processed a retry
  • Why webhooks retry (network failures, timeouts, crashes)
  • Why this matters MORE with payment processors (Stripe, PayPal, etc.)
  • The simple solution: idempotency keys and deduplication logic
  • What you’ll learn: production-ready pattern you can copy/paste

The Problem: Webhook Retries Are Normal (200 words)

  • Stripe (and most webhooks) retry failed deliveries automatically
  • Retry schedule: immediate, 1h, 6h, 24h, 3d (varies by provider)
  • Common failure scenarios:
    • Your n8n server was restarting
    • Database connection timeout
    • API rate limit hit mid-workflow
    • Network blip during response
  • The webhook sender CAN’T know if you processed the event before failing
  • Result: same event delivered 2+ times
  • Without deduplication: same action happens twice (charge customer, send email, create record)

Why Simple “Check if Exists” Isn’t Enough (150 words)

  • Race condition: two webhook deliveries arrive 100ms apart
  • Both check database → both see “doesn’t exist” → both process
  • This is REALLY common with high-volume webhooks
  • Database-level uniqueness constraints help but don’t prevent workflow execution
  • Need atomic “check-and-set” operation

The Idempotency Pattern (300 words)

What is Idempotency?

  • Computer science principle: operation can be repeated safely
  • Same input → same result, no duplicate side effects
  • Payment example: charging customer twice vs recording charge once and returning same receipt

How It Works in n8n:

  1. Extract unique event ID from webhook payload (Stripe: id field)
  2. Atomic check: Try to INSERT event ID into deduplication table with unique constraint
  3. If INSERT succeeds → first time seeing this event → process normally
  4. If INSERT fails (duplicate key error) → already processed → return 200 OK and exit
  5. Return success response AFTER processing completes

Key Implementation Details:

  • Use database unique constraint, NOT application-level check
  • Store: event_id, processed_at timestamp, workflow_execution_id (for debugging)
  • Add TTL/cleanup: delete records older than 90 days (Stripe’s event retention)
  • Return 200 OK even for duplicates (tells Stripe to stop retrying)

n8n Workflow Implementation (400 words)

Workflow Structure:

Webhook Trigger

Extract Event ID

Postgres INSERT (with ON CONFLICT DO NOTHING)

IF: Check if inserted (row count > 0)
  ↓ YES (first time)
  Process Event (Stripe API, update customer, send email, etc.)

  Update dedupe record with status="completed"
  ↓ NO (duplicate)
  Skip processing

Respond 200 OK

Step-by-Step Setup:

1. Create Deduplication Table (Postgres):

CREATE TABLE webhook_events (
  event_id VARCHAR(255) PRIMARY KEY,
  source VARCHAR(50) NOT NULL, -- 'stripe', 'github', etc.
  processed_at TIMESTAMP DEFAULT NOW(),
  status VARCHAR(20) DEFAULT 'processing',
  workflow_execution_id VARCHAR(255),
  payload JSONB
);

CREATE INDEX idx_processed_at ON webhook_events(processed_at);

2. Webhook Node Configuration:

  • Path: /webhooks/stripe
  • Method: POST
  • Response: “Webhook Received” (we’ll customize later)
  • Authentication: Stripe signature verification (separate guide)

3. Extract Event ID (Code Node):

const eventId = $input.item.json.id; // Stripe event ID
const source = 'stripe';

return {
  eventId,
  source,
  payload: $input.item.json
};

4. Dedupe Check (Postgres Node):

  • Operation: Execute Query
  • Query:
INSERT INTO webhook_events (event_id, source, payload, workflow_execution_id)
VALUES (
  '{{ $json.eventId }}',
  '{{ $json.source }}',
  '{{ $json.payload }}'::jsonb,
  '{{ $execution.id }}'
)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
  • Continue on Fail: YES (important!)

5. IF Node:

  • Condition: {{ $json.event_id !== undefined }}
  • True = new event, False = duplicate

6. Process Event (True Branch):

  • Your actual business logic here (charge customer, update DB, etc.)
  • End with Postgres update:
UPDATE webhook_events
SET status = 'completed'
WHERE event_id = '{{ $('Extract Event ID').item.json.eventId }}';

7. Respond (both branches merge here):

  • Respond to Webhook node
  • Status Code: 200
  • Body: {"received": true}

Testing the Pattern (200 words)

Manual Duplicate Test:

  1. Trigger webhook from Stripe dashboard
  2. Check n8n execution → should process normally
  3. Use Stripe dashboard “Resend Webhook” button
  4. Check n8n → should skip processing but return 200
  5. Verify in Postgres: only 1 record with status=“completed”

Load Testing:

  • Use tool like Artillery or k6 to send 100 concurrent webhook POSTs with same event ID
  • Verify only 1 record created
  • Check for any race condition failures

Monitoring:

  • Count duplicate events daily: SELECT COUNT(*) FROM webhook_events WHERE ...
  • Alert if duplicate rate > 5% (indicates wider issues)
  • Track processing failures vs duplicates separately

Alternative Approaches (150 words)

Redis-based deduplication:

  • Faster than Postgres for high-volume
  • Use SETNX (set if not exists) command
  • TTL built-in (auto-expire old keys)
  • Trade-off: less durable (data loss if Redis crashes)

n8n’s built-in deduplication:

  • Some trigger nodes have deduplication (Webhook doesn’t as of 2026)
  • Not suitable for cross-workflow deduplication
  • Check node docs for availability

Application-level locks:

  • Use distributed locks (Redlock, database locks)
  • More complex to implement correctly
  • Usually overkill for webhook use case

Common Mistakes (150 words)

❌ Returning 4xx/5xx for duplicates (triggers more retries) ❌ Checking existence without unique constraint (race conditions) ❌ Processing before deduplication check ❌ Not handling DB connection failures gracefully ❌ Forgetting to clean up old records (table grows forever) ❌ Using workflow execution ID as dedupe key (same event can trigger multiple workflows)

✅ Always use provider’s event ID as dedupe key ✅ Return 200 OK for duplicates ✅ Atomic database operations ✅ Cleanup job for old records ✅ Monitor duplicate rates

Production Checklist (100 words)

  • Dedupe table created with unique constraint
  • Event ID extracted from webhook payload
  • INSERT with ON CONFLICT before processing
  • Both branches return 200 OK
  • Cleanup job scheduled (weekly/monthly)
  • Monitoring dashboard tracking duplicate rate
  • Load tested with concurrent requests
  • Alerts for processing failures
  • Documentation updated with webhook patterns
  • Team trained on debugging duplicate events

Conclusion (100 words)

  • Webhook idempotency isn’t optional for production systems
  • The pattern is simple: atomic check, process once, always return 200
  • 30 minutes to implement, prevents expensive mistakes forever
  • Especially critical for payment processors (Stripe, PayPal)
  • Same pattern works for ANY webhook provider (GitHub, Shopify, etc.)
  • n8n makes it easy with Postgres node and conditional logic
  • Copy this pattern into your workflow template library

Content Notes

  • Include full n8n workflow JSON export (downloadable)
  • Screenshot of workflow in n8n editor
  • Diagram: normal flow vs duplicate flow side-by-side
  • Code snippets for Postgres, Redis, and MongoDB variants
  • Link to Stripe webhook documentation
  • Mention webhook signature verification (separate security concern)
  • Real-world incident story (anonymized): “How we accidentally charged 50 customers twice”

SEO Strategy

  • Primary: “n8n webhook idempotency”
  • Secondary: “prevent duplicate webhook processing”, “Stripe webhook deduplication”, “n8n webhook best practices”
  • Long-tail: “why am I getting duplicate Stripe charges in n8n”
  • Target: devs implementing payment webhooks, n8n users in production

Promotion

  • Post in n8n community (reply to the original thread)
  • r/n8n, r/webdev with title “PSA: Don’t process webhooks without idempotency”
  • Twitter: “Just saw someone get hit with duplicate Stripe charges from webhook retries. Here’s the 5-line fix:”
  • HN: “Show HN: Simple pattern to prevent duplicate webhook processing”

Want this built for you?

We design and ship production n8n automation for agencies, and train your team to own it.

Book a build →