Skip to content
,
Automation platforms from scratch (n8n, Zapier, and friends) · Part 10

How to make workflow automations reliable with retries and human approvals

7 min read
Analytics Made Simple: Production Reliability in Workflow Automation: Retries, Idempotency, and Human Approvals

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:

Four defenses: repeat-safe writes, backoff with jitter, holding what keeps failing, and asking a person
The four defenses working together. The $500 refund threshold is an example; pick a limit that fits your business. The 86400-second lock matches the 24-hour expiry in the code below.
  • 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.

Instant retries hit a struggling service in waves; backoff with jitter spreads retries out
Figure 2: Instant retries arrive in waves. Growing, random waits spread them out.

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 seconds

In 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.

Four kinds of error and their fixes: short outage, duplicate, rate limit, bad data
Figure 3: Short outages get retries, duplicates get a lock, rate limits get a pause, and bad data gets held for a person. A 400 is the code a web service returns when the request itself is wrong, such as a field that does not match the expected layout.
  • 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

Written by

Jose S

Founder & Lead Analyst · Analytics Made Simple

Hands-on data strategist, analytics engineering lead, and educator. Writing practical, no-fluff guides to help everyday teams, analysts, and engineers master SQL, AI systems, and modern data architectures.

Keep going

Same lessons in your feed

Short diagrams, hooks, and weekly tutorials on Substack, Instagram, X, and Facebook.

Google Search Prefer our practical guides in Google Search & Top Stories: