What is Classify Errors Before Retry?
Classify Errors Before Retry is an AI refactoring for LLM systems, in the Safety and reliability group. Separate transient errors from permanent ones and retry only what can succeed.
The assistant schedules a Pix transfer. The bank takes a moment too long, and the call times out. So the code does what it always does. It tries again. Five times.
But the bank had already recorded the first one. The response just got lost on the way back. Five transfers go out, for one request.
In the last lesson, you saw this smell: blind retry. Now we'll fix it, step by step, with a number before and a number after. You'll leave knowing which errors deserve a second attempt, and how to make that second attempt harmless.
The idea fits in one sentence. Before you retry, classify the error.
A transient error can go away on its own. An unstable network, a rate limit, a service that's briefly down. Those get retries, with a growing wait and a cap.
A permanent error won't. Invalid input, no permission, not enough balance, a business rule. Those go back to whoever called, with an explanation they can act on.
And every call that changes the world carries an idempotency key. That's the piece that makes a retry safe.
Before, there was one arrow. Any error went straight back to the call. After, the type of error picks the path.
The call goes out with a key. The service uses it to recognize a repeat.
The error is classified by its code or status. Never by searching the message text for the word timeout. Messages change. Codes don't.
Only transient errors loop back, with a growing wait and a cap.
Permanent errors become a clear answer. For the user, or for the model, so it can change course instead of insisting.
And success happens once, even when there were retries.
Let's walk through the dangerous case, the timeout that was actually a success.
A user asks to schedule two hundred and fifty reais for tomorrow.
The moment they ask, the system creates a request id. Not inside the retry loop. At the click.
The bank processes the transfer. Then the network drops the reply.
Your code sees a timeout. That's transient, so it retries. With the same key.
The bank recognizes the key, and instead of scheduling again, it returns the first result.
The trace shows two attempts, and one effect. That's the number you want to see.
Treating the timeout as transient was right. What stopped the duplicate was a key born with the user's intent, not with the attempt. Serious payment APIs accept that key for exactly this reason.
Here's one for you. An email API can send back five errors. Too many requests. Invalid recipient. Service unavailable. Invalid API key. And duplicate key, message already sent. What do you do with each one?
Too many requests is transient. Wait as long as the service asks, then try again, within your cap.
Invalid recipient is permanent. Tell the user, or the model, that the address needs fixing.
Service unavailable is transient. Retry, with a growing wait between attempts. If it keeps failing past the cap, stop and fall back.
Invalid API key is permanent, and it's a configuration problem. No call will ever work, so don't retry. Alert the team.
And duplicate key is a success in disguise. The email already went out. Treat it as done, and don't send it again.
As with every refactoring here, start by measuring.
Pull from your logs the most common errors for each dependency, how many retries each one caused, and any duplicated effects. Two transfers for the same request is the number that matters.
Classify each service's error codes as transient or permanent. When in doubt, treat it as permanent. A wrong no is cheaper than a wrong yes.
Add an idempotency key to every call with a side effect, generated once per user intent.
Then retry only transient errors, with growing waits, a cap, and a little randomness in the wait. That randomness is called jitter, and it stops a thousand clients from retrying in the same second.
Write useful messages for permanent errors, for the user and for the model. Not enough balance on the account is something a person can act on. Error is not.
In the agent loop, add a repetition detector. The same tool, with the same arguments, after a permanent error, ends that path.
And measure again. Retries per error type, time to answer, and duplicated effects. That last one should be zero.
The first risk is the most common bug in this refactoring. The key is generated inside the retry loop. Every attempt gets a new key, the service sees new requests, and idempotency is gone. The key is born with the click, the ticket, the conversation, never with the attempt.
The second is stacked retries. The provider's SDK retries twice. Your function retries three times. The agent calls the tool again. That's nine attempts, and nobody agreed on that number. Turn off the SDK retry on calls you classify yourself. One place decides.
And some services don't accept a key at all. Then check the state before repeating. Is there already a transfer for this request? If you can't check, don't retry a side effect. Hand it to a person instead.
So here's the idea to take with you. A good retry policy is mostly about saying no. No to errors that time won't fix, no to new keys on old intentions, and no to a third layer of retries. What's left is a second attempt that can never do harm.
In the next lesson, we'll see everything from this trail working together in a real system: invoice and contract extraction. See you there.
Classificar Erro Antes de Tentar de Novo
Classify Errors Before Retry is an AI refactoring for LLM systems, in the Safety and reliability group. Separate transient errors from permanent ones and retry only what can succeed.
The assistant schedules a Pix. The bank API returns a timeout. Code retries five times with the same call. The bank had already recorded the first one. Five Pix transfers go out. Every error triggers another attempt, including errors that will never succeed, and side effects end up duplicated. This is the Blind Retry smell.
Before retrying, classify the error. Transient errors (network, rate limit, momentary unavailability) get retries with growing backoff and a limit. Permanent errors (validation, permission, balance, business rule) go back to the caller with an explanation. Every side-effect call carries an idempotency key.
-for attempt in range(5):
- try:
- return bank.schedule_pix(key, amount, date)
- except Exception:
- time.sleep(2 ** attempt)
+TRANSIENT = {"TIMEOUT", "CONNECTION_RESET", "RATE_LIMITED", "SERVICE_UNAVAILABLE"}
+
+def schedule_pix(key, amount, date, request_id):
+ for attempt in range(3):
+ try:
+ return bank.schedule_pix(key, amount, date, idempotency_key=request_id)
+ except BankError as e:
+ if e.code not in TRANSIENT:
+ return {"ok": False, "error": e.code, "retryable": False,
+ "message": HUMAN_MESSAGES.get(e.code, e.message)}
+ time.sleep(min(2 ** attempt + random.random(), 10)) # backoff with jitter
+ return {"ok": False, "error": "UNAVAILABLE", "retryable": True,
+ "message": "The bank is unstable. Try again in a few minutes."}
Before: any error, same response.
flowchart LR
C["1. Call"]:::tool --> E{"2. Error?"}
E -->|"any"| C
After: the error type decides.
flowchart LR
C["1. Call with idempotency key"]:::tool --> E{"2. Error type"}
E -->|"transient"| W["3. Wait and retry, with a limit"]:::tool
W --> C
E -->|"permanent"| R(["4. Explain to the caller"]):::io
E -->|"ok"| OK(["5. Result"]):::io
Retrying only helps when the error’s cause can go away on its own. Unstable networks and rate limits pass; insufficient balance and an invalid Pix key do not. Classifying avoids spending attempts on what will not work, and avoids an attempt “succeeding” at the wrong moment.
The idempotency key solves the most dangerous case: a call that succeeded on the server, but whose response was lost on the network. Without the key, the retry repeats the effect. With it, the server recognizes the repeat and returns the first result. Serious payment APIs (Stripe’s, Brazilian banks’ Pix APIs) accept that key for this reason.
In an agent, a well-explained permanent error also helps the model: instead of repeating schedule_pix with the same arguments, it informs the user or changes course. Classify by code (INSUFFICIENT_FUNDS, 400, 429), never by if "timeout" in str(e). Messages change; codes do not.
Input: schedule R$250 for tomorrow, request_id=chk_9f3a. The bank processes it, the network drops before the response. With no key, the retry creates a new chk and schedules again. With a key, the second POST returns the same schedule. Output on the trace: attempts=2, effect=1.
Treating the timeout as transient is right. What stops the duplicate is the key born with the user’s intent, not with the attempt.
Retry-After when the service sends it.The provider SDK retries twice. Your function retries three times. The agent loop calls the tool again. Nine attempts, and nobody agreed on the cap. Turn off the SDK retry (max_retries=0) on calls you classify. One place decides.
Another trap: generating uuid4() inside the for attempt. Each turn is a new key, and idempotency vanishes. The key is born with the click, the ticket, the conversation’s request_id.
If the service does not accept a key, check state before repeating (“is there already a Pix with this request_id?”). With no check, do not retry a side effect.
Classify these errors from an email-sending API and say what to do with each one.
429 Too Many Requests
400 Invalid recipient address
503 Service Unavailable
401 Invalid API key
409 Duplicate idempotency key (message already sent)
429: transient. Wait (respecting Retry-After) and try again, with a limit.400 invalid address: permanent. Tell the model or the user the address needs fixing.503: transient. Retry with backoff; if it persists, open the breaker and use the fallback.401: permanent and a configuration problem. Do not retry; alert the team, because no call will work.409 duplicate key: a disguised success. The email was already sent; treat it as a positive result, without resending.Answer the 5 questions to complete this lesson. Watching alone does not complete it: the check is what counts toward the trail and the certificate.
Classify Errors Before Retry is an AI refactoring for LLM systems, in the Safety and reliability group. Separate transient errors from permanent ones and retry only what can succeed.
It treats Blind Retry.
The Circuit Breaker and Idempotent Action design pattern.