An automation you rely on needs three safety habits. When a step fails, it should wait a little longer before each new try. If the same request arrives twice, it should only act once. And big or unusual actions should wait for a person to say yes. Trying again instantly, over and over, can turn a short glitch into double charges.
Say your payment service has a two-second glitch, and your automation tries the payment again five times in that one second. Some of those tries get through, and a customer is charged more than once. A check that skips repeats, plus a short random wait between tries, would have made that glitch harmless.
This is the last post in the series. A workflow here means one automation, a chain of steps that runs on its own in a tool such as n8n, Zapier, or Make.
Four defenses that work together
Treat occasional failure as normal, and build four defenses that cover each other’s gaps:

- Repeat-safe writes: a step that changes data should give the same result whether it runs once or five times, so a duplicate delivery or a retry never corrupts a record.
- Backoff with randomness: each retry waits longer than the last, plus a random amount, so a struggling service gets room to recover.
- A holding queue for failures: when retries run out, save the data and the error so nothing is lost, and someone can replay it after a fix.
- A person for big decisions: large refunds, account deletions, or uncertain AI decisions wait for an approval click.
They only work together. Retries without repeat-safe writes cause duplicate charges. A holding queue nobody watches becomes a graveyard. An approval step with no deadline can stall work for days.
Why instant retries make outages worse
Retrying the moment something fails feels sensible. But when many workflows fail at once, they all retry at once, and the struggling service gets hit again.

Say your customer database freezes for eight seconds during maintenance, and two hundred workflows try to write to it in that window. All of them fail. If all two hundred retry one second later, and again a second after that, the database faces hundreds of requests in a few seconds just as it tries to recover, and a short freeze can stretch into a longer outage. Engineers call this the thundering herd problem.
The fix combines two ideas: a wait that doubles with each attempt, and a random offset called jitter. Amazon Web Services (AWS), Amazon’s cloud computing business, published a widely cited post on backoff and jitter that calls this version “full jitter”:
delay = random_between(0, min(max_delay, base_delay * 2 ^ attempt))Here it is in JavaScript, which you can use in an n8n Code step or your own script:
// Resilient retry delay calculator with Full Jitter
function calculateBackoffDelay(attempt, baseSeconds = 2, maxSeconds = 300) {
// Calculate exponential ceiling: 2^attempt * baseSeconds
const exponentialCeiling = Math.min(maxSeconds, baseSeconds * Math.pow(2, attempt));
// Apply full randomized jitter between 0 and ceiling
const jitteredDelayMs = Math.floor(Math.random() * exponentialCeiling * 1000);
return jitteredDelayMs;
}
// Attempt 0: 0 to 2 seconds
// Attempt 1: 0 to 4 seconds
// Attempt 2: 0 to 8 seconds
// Attempt 3: 0 to 16 seconds
// Attempt 4: 0 to 32 secondsIn plain words: the window doubles with each attempt, up to a cap of 300 seconds, and the function picks a random point inside it. Tested 2,000 times per attempt, every delay fell inside the windows in the comments, from 0 to 2 seconds on the first retry up to 0 to 32 seconds on the fifth. Because each of the two hundred workflows picks its own random wait, their retries spread out over time instead of arriving together.
Making writes safe to repeat
Services such as Stripe can send the same webhook more than once. A webhook is a message a service sends your workflow when something happens. Stripe’s webhook documentation advises guarding against duplicate events, because a delivery may be retried if your server is slow to answer.
Without a guard, two copies of the same payment event can run at the same time. Each creates an invoice and charges the card, and the customer is billed twice. The guard is to record each event’s unique ID the first time you see it, and skip any event whose ID you have already recorded.
This code does that with Redis, a fast in-memory data store. It uses the SET command with two options: NX, which only sets the key if it does not exist yet, and EX, which deletes it after 86,400 seconds, one day:
// Inbound Webhook Idempotency Check in n8n Code Node
const redis = require("redis");
const client = redis.createClient({ url: process.env.REDIS_URL });
async function enforceIdempotency(eventId) {
await client.connect();
const lockKey = `idempotency:webhook:${eventId}`;
// Atomic SET with NX (only if not exists) and EX (expire in 86400s / 24 hours)
const acquired = await client.set(lockKey, "processing", { NX: true, EX: 86400 });
if (!acquired) {
// Key already exists! This is a duplicate delivery.
console.log(`Duplicate event detected: ${eventId}. Skipping downstream execution.`);
await client.disconnect();
return { isDuplicate: true, status: "SKIPPED_DUPLICATE" };
}
await client.disconnect();
return { isDuplicate: false, status: "ACQUIRED_LOCK" };
}In plain words: it tries to store a key named after the event ID. If the key already exists, another run has handled or is handling that event, so this run reports a duplicate and the workflow should reply “OK” and stop. To use an outside package such as redis in an n8n Code step on your own server, n8n’s documentation on using modules in the Code node explains the setting that allows it.
Match each kind of error to the right fix
Different errors need different responses. Retrying bad data never helps, and giving up on a brief outage loses work that would have succeeded a moment later.

- Short outages: timeouts and server errors often clear on their own. Retry with backoff and jitter, and stop after a few attempts.
- Duplicates: when the same event arrives twice, the lock above catches it before any business logic runs.
- Rate limits: when a service says to slow down, often with a 429 error and a wait time, pause for that long and queue the rest. Retrying at once only extends the penalty.
- Bad data: when a service renames or removes a field, retries will keep failing. Save the data and the error in a holding queue, often called a dead-letter queue, and alert the owner to fix the workflow.
Bringing in a person
Some actions are too risky to run unattended. A $10 credit for a billing error can be automatic. A $2,450 credit, or deleting an account with fifty users, should wait for someone to look.
Instead of acting, the workflow posts a card in Slack or Microsoft Teams showing the customer, the amount, the reason, and two buttons. This is a Slack Block Kit card in JSON, a plain text data format:
[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Approval needed*\n*Customer:* Enterprise Corp\n*Amount:* $2,450.00\n*Reason:* Annual prepayment contract adjustment"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "Approve" },
"style": "primary",
"action_id": "approve_transaction",
"value": "TXN-98412"
},
{
"type": "button",
"text": { "type": "plain_text", "text": "Reject" },
"style": "danger",
"action_id": "reject_transaction",
"value": "TXN-98412"
}
]
}
]
When someone clicks Approve or Reject, Slack sends the click to a second workflow, which carries out or cancels the action and records who decided. Give these requests a deadline, so an unanswered approval does not stall work forever.
What to do next
- Check your retry settings: find any step that retries instantly and switch it to growing, random waits, because instant retries are the easiest way to turn a hiccup into an outage.
- Add a duplicate check: put an event-ID lock at the start of any workflow that charges money or creates records.
- Set up a holding queue and an alert: make sure failed runs are saved and that someone is told about them.
- Pick your approval threshold: decide the amount above which a person must approve, and add the approval card for those cases.
Series notes
This is Part 10 of Automation platforms from scratch, and it closes the series. Previous: when to skip platforms.
Sources
- AWS Architecture Blog: Exponential Backoff and Jitter.
- Stripe documentation: webhooks, checked October 2, 2026.
- Redis documentation: SET.
- n8n documentation: modules in the Code node.
- Slack documentation: Block Kit.
Keep going
Same lessons in your feed
Short diagrams, hooks, and weekly tutorials on Substack, Instagram, X, and Facebook.
