Area 08 // BackendLesson 136 / 14014 min read
Retries with backoff and idempotency keys
Retries with exponential backoff and jitter let a client recover from temporary failures such as rate limits and overloaded servers without making things worse, and idempotency keys make it safe to retry operations with side effects (payments, emails, tool actions) because repeats return the original result instead of doing the work again.
Think of it like this
Analogy
Calling a busy helpline: if it's engaged, you don't redial every second (that keeps the lines jammed); you wait a little, then a bit longer each time, and not at exactly the same moment as everyone else. And if you're asking them to send a cheque, you quote your reference number each time you call back, so they don't send three cheques when you only wanted one.
The problem it solves
Networks drop connections, providers return 429 and "overloaded" errors, and servers restart mid-request. Without retries, users see errors for problems that would have fixed themselves in a second. With naive retries (immediate, unlimited, everyone at once), you turn a blip into an outage and repeat actions that already happened: double charges, duplicate emails, two refunds from one agent step.
How it works: retries with backoff
- Classify errors: retry transient ones (connection errors, timeouts, 429, 500/502/503/504, provider overloaded); don't retry permanent ones (400, 401, 403, 404, 422, content policy refusals) (status codes).
- Back off exponentially: wait base × 2^attempt (e.g. 1, 2, 4, 8 s) up to a cap.
- Add jitter (randomness) so clients spread out.
- Respect
Retry-Afterwhen the server sends it (rate limiting). - Bound it: maximum attempts and an overall deadline; then fail over to another model or provider (routing and fallbacks) or return a clear error.
- Use library support: provider SDKs retry some errors automatically (configurable
max_retries); for your own calls use a library such as tenacity (see the code). - Don't stack retries: if the SDK, your code and a gateway each retry three times, one failure becomes 27 calls.
How it works: idempotency keys
- Idempotent means doing it twice has the same effect as doing it once. GET, PUT and DELETE are naturally idempotent; POST usually isn't.
- The client generates a unique key (a UUID) per logical operation and sends it, e.g.
Idempotency-Key: 7f1c.... - The server records the key with the result when it first processes the request (in a database with a unique constraint, or Redis with a TTL).
- Repeats with the same key return the stored result without doing the work again (see the code); a request with the same key but different content is rejected.
- Handle in-progress duplicates: lock or mark the key as processing so two simultaneous retries don't both run.
- Use it for payments, sending messages, creating jobs, and every agent tool that changes something (agent loop, queues), since agents and queues retry steps.
See it in code
- Install
pip install tenacity- Needs
- Python 3.10+. Waits are tiny so the demo runs instantly; real schedules are seconds.
A call that fails twice with 503 and succeeds on the third try, a typical backoff schedule, and an idempotent payment that's safe to retry.
import random
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential_jitter
# RETRIES WITH BACKOFF: retry only transient errors, wait longer each time, add jitter, give up eventually.
class Transient(Exception): ... # e.g. 429, 503, timeouts
class Permanent(Exception): ... # e.g. 400 bad request, 401: retrying won't help
failures = {"left": 2}
@retry(retry=retry_if_exception_type(Transient), stop=stop_after_attempt(4),
wait=wait_exponential_jitter(initial=0.01, max=0.1), reraise=True) # tiny waits for the demo
def call_provider(prompt: str) -> str:
if failures["left"] > 0:
failures["left"] -= 1
print(" attempt failed: 503 overloaded")
raise Transient()
return f"answer to {prompt!r}"
print(call_provider("hello"), f"(attempts: {call_provider.statistics['attempt_number']})")
random.seed(1)
waits = [min(30, 1 * 2 ** n) + random.uniform(0, 1) for n in range(5)] # typical production schedule
print("backoff schedule (s):", [round(w, 1) for w in waits])
# IDEMPOTENCY KEYS: the client sends a unique key; repeats of the same request return the saved result.
SAVED, CHARGES = {}, []
def create_payment(idempotency_key: str, amount: int) -> dict:
if idempotency_key in SAVED:
return {**SAVED[idempotency_key], "replayed": True}
CHARGES.append(amount) # the side effect happens once
SAVED[idempotency_key] = {"payment_id": f"pay_{len(CHARGES)}", "amount": amount}
return SAVED[idempotency_key]
key = "7f1c2e9a-retry-safe" # e.g. a UUID generated by the client
print(create_payment(key, 40))
print(create_payment(key, 40)) # network glitch -> client retries
print("customer charged:", CHARGES) attempt failed: 503 overloaded
attempt failed: 503 overloaded
answer to 'hello' (attempts: 3)
backoff schedule (s): [1.1, 2.8, 4.8, 8.3, 16.5]
{'payment_id': 'pay_1', 'amount': 40}
{'payment_id': 'pay_1', 'amount': 40, 'replayed': True}
customer charged: [40]The second payment call returned the first result, marked as a replay, and the customer was charged once. In production the saved keys live in a database table with a unique constraint, so this also works across servers.
A real example
Example
The agent that refunded twice
A support agent's issue_refund tool called the payments API. One night the payments API was slow; the HTTP client timed out after 10 seconds and the agent loop retried the step. Both requests went through, so dozens of customers got double refunds. The fix: the agent harness generated an idempotency key per tool call (run_id + step + tool), passed it to the payments API, and stored tool results so retried steps reused them. Timeouts were raised for payment calls, and retries for non-idempotent tools were only allowed with a key.
The easy way
Start here
Configure the provider SDK's built-in retries (they back off and respect Retry-After), add tenacity with exponential backoff and jitter for your own external calls, and send an idempotency key on any request that creates or changes something.
When you grow: the production setup
Teams centralise retry policy per dependency (one layer retries, others don't), use deadlines and retry budgets, add circuit breakers and fallbacks, store idempotency keys with request fingerprints and TTLs, make every state-changing endpoint and agent tool idempotent, and monitor retry rates as an early warning of upstream trouble.
Trade-offs
What you gain
- Transient failures become invisible to users.
- Safe retries for actions with side effects.
- Backoff and jitter protect struggling services.
What it costs you
- Retries add latency and load.
- Idempotency needs storage and careful key handling.
- Retrying at several layers multiplies traffic.
When not to use it
- Don't retry permanent errors, or user-visible interactions where a fast failure plus fallback is better than a long wait.
Common mistakes
- Retrying immediately in a tight loop.
- Retrying 400s or other permanent errors.
- Retries at the SDK, app and gateway layers all at once.
- Retrying payments or emails without idempotency keys.
- No overall deadline, so users wait minutes.
Interview questions
Try answering out loud first, then open the model answer.
How should a service retry calls to an LLM provider?
Model answer
Retry only transient failures (timeouts, connection errors, 429, 5xx and overloaded responses) with exponential backoff and jitter, honour Retry-After, cap attempts and total time, avoid stacking retries across layers, then fall back to another model or provider or return a clear error; track retry rates.
What is an idempotency key and how is it implemented?
Model answer
A unique client-generated identifier for a logical operation. The server stores the key with a fingerprint of the request and its result the first time it processes it (with a unique constraint and a processing lock); later requests with the same key return the stored result without repeating side effects, and mismatched payloads are rejected. Keys expire after a retention period.
Why do agents need idempotent tools?
Model answer
Agent loops, harnesses and queues retry steps after timeouts, crashes or model errors, and the model itself may repeat a call. Without idempotency, a retried step can repeat side effects such as payments, emails, tickets or deployments. Idempotency keys per tool call and stored results make retries safe.
Quiz
Pick an answer to see whether it’s right and why.
0 of 5 answered
Key terms
- Retry
- Repeating a failed request, which helps only for temporary errors and should use backoff and a limit.
- Exponential backoff
- Retrying a failed request after waits that double each time (1s, 2s, 4s…), usually with random jitter, so a struggling service can recover.
- Jitter
- Random variation added to retry delays so many clients don't all retry at the same moment.
- Idempotency
- The property that doing the same operation twice has the same effect as doing it once, which makes retries safe (no double charges or duplicate emails).
- Idempotency key
- A unique ID a client sends with a request so the server can recognise repeats and return the original result instead of doing the work again.
Before and after
Progress is saved in this browser only.