Why automatic retries make outages worse, not better
That innocent 'retry on failure' checkbox is one of the most dangerous settings in your automation platform. Left naive, retries send duplicates, double payments, and turn a brief hiccup into a full outage. Here is how we think about backoff, idempotency, circuit breakers, and dead letter queues.
The most dangerous checkbox in your automation platform
There is a small checkbox in almost every automation tool, worded something like "retry this step automatically if it fails." It looks harmless. It sounds responsible. Turning it on feels like the professional thing to do, a seatbelt against the flakiness of the internet.
We have spent enough time cleaning up after that checkbox to have a strong opinion about it. Used without thought, an automatic retry is not a safety net. It is an amplifier. It takes a small, temporary problem and, in the wrong conditions, turns it into a large, expensive one. The number of times we have been called in to explain duplicate invoices, doubled payments, or the same notification sent eleven times over is higher than we would like.
So this is the long version of the conversation we have with clients before we ever tick that box. It is about when a retry helps, when it hurts, and how to tell the difference before it costs you a weekend.
A retry is a bet
Strip away the tooling and a retry is a bet you place against reality. The bet is this: the thing failed once, but the reason was temporary, and if I try again in a moment it will work. That bet only pays off when two conditions hold, and most people only ever check the first one.
The first condition is that the failure really is temporary. A dropped connection, a database that was briefly overloaded, a rate limit that clears in a few seconds. Try again and you get through. A malformed field, an expired token, a record that no longer exists? Those fail the same way every single time. Retrying them just burns quota and time while producing the identical error.
The second condition is the one that quietly wrecks people: the action has to be safe to repeat. If the first attempt already did something before it appeared to fail, then repeating it does that something a second time. A payment goes out twice. An email is sent again. A duplicate record appears. The retry meant to protect you becomes the source of the damage.
There is a word for "safe to repeat," and it is idempotent. An action is idempotent when running it once and running it five times leave the world in the same state. Setting a status to "paid" is idempotent. Charging a card is not. Sending a message is not. Creating a record is only idempotent if you check first whether it already exists. Here is the uncomfortable part: retries are only safe when the action behind them is idempotent, and most of the interesting actions in a business workflow are not idempotent by default. You have to make them so.
Timeouts are not failures. They are question marks.
Before anyone touches retry settings, they should learn to sort failures into three bins, because the bin decides the correct response and confusing them is where most bad retry logic comes from.
The first bin holds clearly temporary failures. A 503 because a server is restarting. A connection that drops mid transfer. A 429 rate limit that tells you exactly when to come back. Retry these, carefully, and you win.
The second bin holds clearly permanent failures. A 400 because the data does not fit the schema. A 401 because credentials are wrong. A 404 because the record you wanted to update was deleted. Retrying these is pure waste. They need a human or a different branch of logic, never the same attempt repeated.
The third bin is the dangerous one, and hardly anyone talks about it: ambiguous failures. A timeout lives here. So does a connection that dies after the request was sent but before the answer came back. The defining feature is that you genuinely do not know whether the other side did the work or not. A timeout does not mean "nothing happened." It means "I don't know what happened." That distinction is enormous, and most platforms treat both cases identically, which is exactly how you end up paying an invoice twice.
Treat the three bins differently. Retry the temporary ones. Route the permanent ones to a person. Retry the ambiguous ones only when the action is idempotent, and otherwise stop and ask.
Why a naive retry makes an outage worse
Picture a service you depend on having a bad moment, failing roughly every other request. Your automation responds with an immediate retry. What have you just done?
You have doubled the load on a service that was already struggling. Instead of giving it room to recover, you push it further down. Now imagine a hundred customers all using that same service, all with immediate retries switched on. The instant it stumbles, it gets hit with two, three, four times the normal traffic. Engineers call this a thundering herd. A brief hiccup becomes a full outage because everyone leans in at once.
The nasty part is the feedback loop. The service slows down, so more requests hit their timeout, so more retries fire, so the service slows down further. A system that drives itself deeper into failure while every individual component behaves "reasonably." That is why a naive retry is not neutral. It is an active accelerant at the worst possible moment.
Then there is the bill, which is easy to forget inside a no-code platform. Every retry consumes an operation, a task, an execution step, depending on how your vendor charges. A workflow that needs ten thousand steps a month in normal operation can burn its entire monthly allowance in one bad afternoon of relentless retries. After that it simply stops, and the part that stops is usually the part that was working fine.
Backoff and jitter: two dials that change everything
Two techniques turn a dangerous retry into a usable one. Both have been standard practice in software for decades, and both are missing from most of the no-code workflows we get called in to fix.
The first is exponential backoff. Instead of retrying immediately and at a fixed rhythm, you lengthen the pause after each failed attempt. First failure, wait one second. Failed again, two. Then four, then eight, then sixteen. The struggling service gets more breathing room with each step, and your automation stops hammering a closed door once a second.
The second is jitter, a deliberately random component in the wait time. The reason is the thundering herd from a moment ago. If every automation waits exactly the same amount, they all come back at exactly the same instant, and you have moved the problem a few seconds into the future rather than solving it. With jitter, one waits 4.2 seconds, the next 5.8, the third 3.1. The herd spreads out, and the recovering service is not immediately trampled again. A small random number, a large effect.
In practice you combine them: a growing wait plus a random top-up. In hand-written code this is three lines. In n8n you can approximate it with a node's retry settings plus a wait node carrying a random value. In Make and Zapier it is clumsier, usually a detour through an error route with staggered pauses. Clumsy, but doable, and the difference on a bad day is the difference between a shrug and an incident.
The circuit breaker: know when to stop
Backoff makes retries polite. But even polite retries are pointless if the service on the other end is fully dead. If a target system has not answered for ten minutes, attempt number 47 with exponential backoff will not change anything except your quota and the length of your error log.
This is where a pattern borrowed from electrical engineering earns its place: the circuit breaker. After a set number of consecutive failures, the system stops trying at all. The breaker trips. For a defined cooldown, no new requests go to the broken service; they are rejected immediately or set aside. Once the cooldown passes, the breaker lets a single test request through. If it succeeds, normal operation resumes. If it fails, the breaker stays open.
You win twice. You spare the struggling service by not bombarding it while it tries to come back up. And you spare yourself the thousands of failed executions you would otherwise have to clean up afterward.
Pure no-code environments rarely ship a ready-made circuit breaker, so you build a rough one: a shared variable or a small record that counts consecutive failures and, past a threshold, flips a switch the automation checks before it runs. That is manual work. Above a certain level of criticality it is work worth doing, and we build exactly this for clients whose processes hang off fragile third party interfaces.
Where do the ones that never get through go?
Every retry strategy eventually runs out. Backoff is exhausted, the breaker has tripped, and there is still a job that simply will not complete. The decisive question is what happens to that job.
The worst answer is nothing. The job vanishes, the error lands in a log nobody reads, and the affected record is left half-finished. An order with no confirmation. A payment with no bookkeeping entry. A lead that never reached the CRM. These silent losses are worse than a loud outage, because nobody notices until a customer complains weeks later.
The better answer has a name: a dead letter queue, the mailbox for undeliverable mail. Anything that permanently fails after all retries is not discarded but collected in a separate list, with everything you need to understand it: which record, which action, which error, which timestamp. A person reviews it on a schedule, decides what to do, and re-triggers the action once the cause is fixed. In no-code terms this is often nothing fancier than a dedicated table your error route writes to. What matters is that it exists and that someone owns the job of looking inside. An automation without this catch mechanism loses data when things go wrong, and usually you find out too late.
A close cousin is the poison message: a single record that crashes every attempt to process it, maybe because a field holds a value the workflow cannot handle. Without a guard, that record sits at the front of the queue forever, processed again and again, failing again and again, blocking everything behind it. A counter that pulls the same record after the third or fourth failed pass and drops it into the dead letter list prevents exactly that gridlock.
What the platforms actually do by default
Theory is one thing. The default settings of your tools are what actually run on the bad day, so let us be specific.
Zapier retries failed steps automatically over a period of time, often without the builder having consciously configured it. Convenient, and fine for many simple cases. It becomes dangerous precisely when the retried step is not idempotent, when it sends or triggers something. That is where the duplicates come from. If you have a Zapier step that sends an email, kicks off a payment, or creates a record, you have to actively make sure a retry cannot fire twice.
Make lets you choose, in the scenario settings, between failing immediately and a retry through its built-in error handlers. More powerful than Zapier, but it asks more of you. We regularly see scenarios with no error handler at all, where one failure stops the whole thing, and the opposite, scenarios that plow on through failures and produce half-written records. Make's rollback handler is genuinely useful, but it only touches the built-in data store operations, not the side effects on external services. No rollback un-sends an email that already went out.
n8n gives you the most control. Per node you configure retries, wait times, and error paths, and the error trigger builds a central handler for the whole workflow. That is the good news. The bad news: because you decide so much, you also own the mistakes. An n8n workflow with no thought given to retries and failure handling is exactly as fragile as any other, just with more knobs to get wrong.
The shared lesson across all three: the default is rarely the right setting. It is tuned for the comfortable normal case, not for the day an interface goes slow. And that day comes.
The ambiguous case, up close
Back to the most dangerous bin, the timeout, because it does the most damage in practice and is the least understood.
Your automation sends a payment instruction to a payment provider. The request goes out, the provider processes it, but before its confirmation reaches you the connection drops. Your automation sees a timeout. Now what?
Retry blindly and you risk a double charge. Do not retry and you risk a payment that may never have gone through. Both options can get expensive, and at that moment you have no information about which situation you are actually in.
The clean way out is an idempotency key. On the first request you send a unique key, an ID that identifies this one payment. When you retry after the timeout, you send the same key again. The provider recognizes it: I have seen this key, the payment is already done, here is the earlier result without charging a second time. Serious payment providers offer this exact mechanism, and automating anything involving money without it is playing with fire.
For actions with no built-in key, the fallback is check first, then act. Before retrying, the automation asks whether the job perhaps already completed. Does the booking already exist? Is the email marked as sent? Is the record already there? Only if the answer is no does it retry. That extra question costs a step and a couple of seconds. It is also the whole difference between a robust automation and a reckless one.
The order we build it in
When we set up a durable retry strategy with a client, we walk a fixed order. It does not replace thinking about the specific case, but it makes sure the important questions get asked.
First, is the action safe to repeat at all? If not, every retry gets an idempotency key or a pre-check in front of it. Without that step we do not add automatic retries, however tempting the checkbox looks.
Second, separate the failure types. Temporary failures may retry, permanent ones are set aside and reported immediately, ambiguous ones are handled with the extra care described above. That distinction goes straight into the branching logic.
Third, set limits. How many retries at most? With what growing gap? When does the breaker trip? There is no single right number, but there is a wrong one: infinite. Every retry needs a ceiling, or the safety net eventually becomes the trap.
Fourth, build the catch. Everything that permanently fails goes into a dead letter list a human reviews regularly, and we make sure that list is visible, not buried in a log nobody opens. Error handling nobody looks at is not error handling.
What we tell clients in the end
A retry is not a checkbox you leave on out of convenience. It is a design decision with consequences, and the heaviest one only shows up on the bad day, when an interface goes slow and your automation has to decide what to do about it.
The good news is you do not need a doctorate in distributed systems to get it right. You need to ask the right questions. Is the action safe to repeat? What kind of failure is this? How many attempts, at what spacing? Where does the permanently failed work go, and who looks at it? Four or five questions that separate an automation which absorbs failure from one that amplifies it.
Most teams answer none of them until after the incident. The whole point of writing this down is that the questions are cheap and the incident is not.