KEEP LEARNING
Build the bigger picture.
The Workflow Engineer connects individual n8n concepts to testing, deployment and running a complete workflow.
Tips > Data, APIs & Webhooks
Webhook senders often retry on timeout or network errors, sending the same event multiple times.
Webhook senders retry on timeouts and network errors, so the same event can arrive several times. To process each event exactly once, extract an idempotency key from a header or the payload, check it against a datastore such as Postgres, Redis, or a Google Sheet before processing, and insert it afterward. Duplicates then get a 200 response so the sender stops retrying.
Webhook senders often retry on timeout or network errors, sending the same event multiple times. Without idempotency handling, you will process duplicates -- charging customers twice, sending duplicate emails, or creating duplicate records. Check for an idempotency key before processing.
Real-world example: A payment gateway retries a payment.completed event three times. Your workflow should process it exactly once.
// Code node: "Check Idempotency"
// Place this immediately after the Webhook node
const idempotencyKey = $input.first().json.headers['x-idempotency-key']
|| $input.first().json.headers['x-request-id']
|| $input.first().json.body.event_id;
if (!idempotencyKey) {
return [{
json: {
duplicate: false,
idempotencyKey: null,
warning: 'No idempotency key provided',
data: $input.first().json.body
}
}];
}
// This value will be checked against the datastore in the next node
return [{
json: {
idempotencyKey,
data: $input.first().json.body
}
}];After the Code node, query your datastore (Google Sheet, Postgres, Redis):
-- Postgres node: Check if this event was already processed
-- Use parameterized queries ($1) to prevent SQL injection
SELECT idempotency_key, processed_at
FROM webhook_events
WHERE idempotency_key = $1
LIMIT 1;
-- Query Parameter 1: {{ $json.idempotencyKey }}Then use an IF node: if the query returned a row, respond with 200 OK (acknowledge but skip). If no row, continue processing and insert the key when done:
-- Postgres node: Mark event as processed (after successful processing)
-- Use parameterized queries to prevent SQL injection
INSERT INTO webhook_events (idempotency_key, processed_at, event_type)
VALUES ($1, NOW(), $2);
-- Query Parameter 1: {{ $json.idempotencyKey }}
-- Query Parameter 2: {{ $json.data.event_type }}Danger: Never use string interpolation in SQL Always use parameterized queries ($1, $2) instead of '{{ $json.value }}' in PostgreSQL nodes. String interpolation is vulnerable to SQL injection -- a malicious webhook payload could execute arbitrary SQL. See Security Tip 7 for details.
This guarantees exactly-once processing. The sender gets 200 for duplicates (so it stops retrying) and the event is processed only on the first delivery.
Related: Set a Unique Encryption Key and Back It Up · Use the HTTP Request Node as a Universal Connector
KEEP LEARNING
The Workflow Engineer connects individual n8n concepts to testing, deployment and running a complete workflow.
APPLY IT TO YOUR SYSTEM
Bring the process, the tools involved and an example of where the current workflow gets stuck.