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:
- Extract unique event ID from webhook payload (Stripe:
idfield) - Atomic check: Try to INSERT event ID into deduplication table with unique constraint
- If INSERT succeeds → first time seeing this event → process normally
- If INSERT fails (duplicate key error) → already processed → return 200 OK and exit
- 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:
- Trigger webhook from Stripe dashboard
- Check n8n execution → should process normally
- Use Stripe dashboard “Resend Webhook” button
- Check n8n → should skip processing but return 200
- 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”
Related reading
Want this built for you?
We design and ship production n8n automation for agencies, and train your team to own it.
Book a build →