Hi guys I have an n8n workflow that processes customer records and sends data to an external API. When the API starts returning 429 Too Many Requests, n8n retries the requests, but during heavy traffic I sometimes end up with duplicate API calls.
My current workflow is basically:
PostgreSQL → Loop Over Items → HTTP Request → Update PostgreSQL
I’m considering storing the API request state in PostgreSQL: CREATE TABLE api_jobs (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL,
status TEXT NOT NULL DEFAULT ‘pending’,
attempts INT NOT NULL DEFAULT 0,
last_attempt_at TIMESTAMP,
external_id TEXT
);
And before sending the request: UPDATE api_jobs
SET
status = ‘processing’,
attempts = attempts + 1,
last_attempt_at = NOW()
WHERE id = $1
AND status = ‘pending’
RETURNING *;
Would you recommend using n8n’s retry settings, a PostgreSQL-backed retry queue, exponential backoff, or a combination of these?
And how would you make the API calls idempotent so that a retry doesn’t accidentally create the same customer/order twice?
Describe the problem/error/question
What is the error message (if any)?
Please share your workflow
(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)
Hi @anon20761130
Retry On Fail reruns the node inside the same execution, so those attempts all finish before Update PostgreSQL runs and attempts never increments. The queue has no record the request went out, which is how the same customer gets sent twice. Run one retry layer, not two: turn Retry On Fail off on the HTTP Request node and set On Error to “Continue (using error output)”. The 429 then arrives as an item on the error branch, Update PostgreSQL records the attempt, and the next scheduled run does the backoff.
The second duplicate source is the claim query. WHERE status = 'pending' leaves a row stuck on ‘processing’ forever if n8n restarts mid-request, so reclaim on age rather than widening the status filter:
WHERE id = $1
AND (
status = 'pending'
OR (status = 'processing' AND last_attempt_at < NOW() - INTERVAL '5 minutes')
)