What is Blind Retry?
Blind Retry is an AI code smell, a sign of trouble in LLM systems, in the Control-flow abusers group. Every error triggers another attempt, including errors that will never succeed.
It's two in the morning. A finance assistant tries to schedule a Pix transfer for a customer. The bank says no.
The code catches the error, waits a little, and tries again. And again. Four times, the same answer: the nighttime limit is exceeded.
On the fifth attempt, the limit had just reset, and the transfer went through. But the customer had already been told it failed. So they paid again, through the bank's app. They paid twice.
The retry worked. That's the problem. In this lesson, you'll learn to tell an error worth retrying from one that's a no, and why that difference has to live in your code.
Here's the shape. The call goes out.
Any error comes back, and every error is handled the same way.
The same call is repeated, with the same arguments, without anyone asking why it failed.
And when it finally passes, the effect may happen twice, or at the wrong time.
The nighttime limit isn't a network blip. It's a business rule that means: not now. Insisting until the clock changes turns that no into an effect nobody asked for.
This smell hides in two places. In code, it's a handler that catches any exception and loops.
In an agent, it's the model. It reads the word error, and calls the same tool again, with the same arguments. In both cases, whatever decided to retry never classified the error.
Wrapping a call in a retry loop is the best known recipe for flaky networks. It looks like protection. And the recipe leaks into errors it was never meant for: no balance, no permission, invalid input.
Errors come back as text, with no type. Your code can't tell try again from this will never work.
The agent learns to insist. If the tool only says error, it looks broken, and the model does the reasonable thing. It tries again.
And nobody thought about idempotency. The tool was written as if every call were unique. Then a timeout that happened after the bank accepted the transfer becomes a second transfer.
The worst cost is the duplicated effect. Two payments, two emails, two orders. With a payment, the refund becomes hours of support, and a customer who stops trusting you.
Then the effect at the wrong time. A retry that recovers from a nighttime limit schedules a payment the user already treated as cancelled.
Then waste. Picture ten thousand rejected requests a day, each tried five times. That's forty thousand extra calls, all useless. And hammering a struggling service can get you blocked, or make its outage worse.
And the diagnosis gets confusing. The trace shows five failures and one success, and nobody reads the first message, the one that said why.
Here's one for you. You've already fixed the obvious part. You only retry on timeouts and server errors, and you send an idempotency key. And a customer still got two billing emails. What do you look at first?
Whether the key changes on every attempt. Idempotency only holds if the key stays the same across every attempt of the same operation. If each retry mints a new key, the email service did exactly what you asked. It sent two emails.
Switching email providers doesn't help, and neither does raising the attempt cap. The duplicate already went through. The fix is a key that belongs to the operation, not to the attempt.
The treatment is a refactoring called classify errors before retry. It has three parts, and skipping any of them leaves a hole. The first is to classify.
A timeout, a server that's down, or a rate limit is transient. Retry, with backoff, a low cap, and if the server says how long to wait, wait that long.
Invalid input is permanent. The request is wrong, and it will be wrong on the eighth attempt too. A classic beginner trap is retrying a bad request because the API sometimes flakes. Network flakes look like timeouts and server errors. A bad request means fix the request.
No balance, a limit, or no permission is permanent, at least for now. Don't loop. Tell the user, or reschedule on purpose.
And a timeout on a write is the tricky one. You don't know if it happened. Retry, but only with the same idempotency key, so the service can say: I already did this one.
In an agent, the classification also has to reach the model. The tool returns an error it can use. A code, whether it's retryable, and what's missing. A bare error pushes the model straight back to the same call.
And add a repetition detector in code. The same tool, with the same arguments, three times in a row, ends the loop. That catches the case where the model ignores the message.
When is a simple retry fine? On a read with no side effect, retrying transient errors with a limit and backoff is good practice. A search that times out can run again.
And the model provider's SDK that retries twice on rate limits and server errors isn't this smell. It's selective. Just check what it retries. An SDK that handles the network doesn't excuse a catch-all you wrapped around a payment.
But notice: the smell doesn't need a duplicated effect. A read job that retries access denied and invalid file eight times has no side effect. It's still blind. It just burns quota instead of money.
So here's the idea to take with you. A retry isn't a safety net you throw over everything. It's a decision you make about one kind of error, the kind that time actually fixes. Everything else needs an answer, not another attempt. And anything that changes the world carries a key that makes the second attempt harmless.
In the next lesson, we'll do that refactoring step by step, on a real payment flow: classify errors before retry. See you there.
Retry Cego
Blind Retry is an AI code smell, a sign of trouble in LLM systems, in the Control-flow abusers group. Every error triggers another attempt, including errors that will never succeed.
A finance assistant schedules Pix transfers (Brazil’s instant payment rail). The code that calls the bank (anonymized):
for attempt in range(5):
try:
return bank.schedule_transfer(key, amount, date)
except Exception as e:
log.warning(f"attempt {attempt} failed: {e}")
time.sleep(2 ** attempt)
raise PaymentFailed()
The log from one early morning:
02:14:03 attempt 0 failed: NIGHT_LIMIT_EXCEEDED
02:14:05 attempt 1 failed: NIGHT_LIMIT_EXCEEDED
02:14:09 attempt 2 failed: NIGHT_LIMIT_EXCEEDED
02:14:17 attempt 3 failed: NIGHT_LIMIT_EXCEEDED
02:14:33 attempt 4 ok
By the fifth attempt the nighttime limit had reset, and the transfer went through. The customer, told of the failure minutes earlier, had already paid through the app. They paid twice.
The retry “worked”. That is the problem. NIGHT_LIMIT_EXCEEDED is not a network blip. It is a business rule: not now. Insisting until the clock changes turns a “no” into an effect nobody asked for again.
Signs of a Blind Retry:
flowchart LR
C["1. Call"]:::tool --> E{"2. Error?"}
E -->|any error| R["3. Try again"]:::tool
R --> C
E -->|"passed by chance"| D[("4. Duplicated effect")]:::store
Two shapes produce the same smell. In code, an except Exception wraps the call. In an agent, the model reads “error” and calls the same tool again. In both cases, the thing that decides to retry never classified the error.
retryable: false, the tool looks broken, and the model tries the same thing.Suppose the bank API (or the HTTP underneath) returns codes. The table below is the map the blind loop does not have:
| Signal | Type | What to do |
|---|---|---|
| Timeout, 503, connection reset | Transient | Retry with backoff, low cap |
429 with Retry-After |
Transient | Wait as long as the server asked |
| 400, validation, invalid Pix key | Permanent | Do not retry. Fix the request |
| Balance, limit, permission | Permanent (for now) | Do not retry. Tell the user or reschedule |
| 409, “already exists” | Idempotent | Treat as success |
| Timeout on a write, outcome unknown | Indeterminate | Retry only with the same idempotency key |
The classic beginner trap: treating 400 as transient “because the API sometimes flakes”. Network flake shows up as timeout or 5xx. 400 means the request is wrong.
| Refactoring | Choose it when |
|---|---|
| Classify Errors Before Retry | Always. Transient errors (network, 429, 503) get retries with backoff; permanent errors go back to the user or the model with an explanation. Side-effect calls use an idempotency key. Targets: Circuit Breaker and Idempotent Action. |
Treatment has three parts, and skipping any of them leaves a hole:
Retry-After. Do not repeat the same arguments after a permanent error.In the agent loop, the tool needs to return an error the model can use: a code, whether it is retryable, and what is missing. “ok” and “error” push the model back to the same call. A repetition detector (the same tool, the same arguments, three times) cuts the case where the model ignores retryable: false.
Before and after, in the loop:
# before: any exception becomes another attempt
# after: only transient, and the key does not change between attempts
result = bank.schedule_pix(key, amount, date, idempotency_key=key)
# if NIGHT_LIMIT_EXCEEDED: return error to the user, no loop
# if timeout: retry with the same idempotency_key
except Exception on top of the Pix.Answer the 6 questions to complete this lesson. Watching alone does not complete it: the check is what counts toward the trail and the certificate.
Blind Retry is an AI code smell, a sign of trouble in LLM systems, in the Control-flow abusers group. Every error triggers another attempt, including errors that will never succeed.
With Classify Errors Before Retry. Each refactoring has safe step-by-step mechanics with an eval before and after.
In the real-world scenarios Test generator and Personal finance assistant with approval.