Recommended resources
The freely available resources this lesson will direct you to.
Welcome back! In our previous lesson, we implemented token-aware rate limiting, a crucial mechanism for managing the cost and load of an LLM service. A key part of that system was returning a 429 Too Many Requests error when a user exceeded their quota. This type of error is a perfect example of a transient failure—a temporary issue that is likely to be resolved if we simply wait and try again.
Today, we'll focus on how to build robust client applications that can gracefully handle these and other transient failures. Your goal for this lesson is to design and implement a retry mechanism with exponential backoff for transient inference failures. As an SRE, you're familiar with the principle that in any distributed system, temporary failures are not the exception; they are the norm. We'll apply this mindset to build a resilient LLM client.
1. Identifying When to Retry
The first rule of implementing retries is to know when not to. Retrying a request that failed due to a permanent error, like an invalid API key, is wasteful and can hide underlying bugs. We only want to retry transient errors, where the issue is temporary.
In the context of calling an LLM API, these errors typically fall into a few categories:
- Server Overload / Rate Limiting: The service is temporarily too busy to handle your request. This is indicated by an HTTP
429 Too Many Requestsstatus code. - Server-Side Errors: A temporary glitch occurred on the server. These are usually signaled by HTTP
5xxcodes (e.g.,500 Internal Server Error,503 Service Unavailable). - Network Issues: The request may have timed out or been interrupted due to network problems. This can manifest as an HTTP
408 Request Timeoutor a client-side connection error.
The Google Cloud documentation for their Generative AI services provides a concise list of which errors are considered retryable.
Retry strategy | Generative AI on Vertex AI
This document from Google outlines the best practices for retry strategies when interacting with their AI models.
Focus on the section "Identifying retryable errors." Pay attention to the HTTP codes listed under "Response" and the discussion on idempotency. For most LLM inference calls (generateContent), the operation is considered safe to retry even though it's not strictly idempotent, as it doesn't change server-side state.
The key is to classify errors and only trigger the retry logic for those that are transient. This flow diagram visualizes the decision process.

A flow diagram showing the client-side retry process. An initial request is made. If it fails, the error is classified. If it's a retryable error, the client waits for a calculated delay (using backoff and jitter) before making the next attempt. If it's not retryable, it fails fast.
2. The Problem with Naive Retries: The Thundering Herd
Your first instinct might be to implement a simple loop: if a request fails, wait one second, and try again up to five times. This is a good start, but it has a dangerous flaw at scale.
Imagine your LLM service is temporarily overloaded and starts failing requests. If hundreds of clients all use the same "wait 1 second and retry" logic, they will all hit the service again at the exact same time. This flood of synchronized retries, often called a "thundering herd" or a "retry storm," can prevent the service from ever recovering.
The following video provides an excellent explanation of this problem in the context of a microservices architecture, which is directly applicable here.
Retries & Exponential Backoff - Deep Dive
This video, "Retries & Exponential Backoff - Deep Dive" from the channel glich.stream, gives a great conceptual overview of why simple retries are problematic.
Watch the segment from the problem with retries. The speaker uses diagrams to show how a simple, fixed-delay retry strategy across many clients can amplify the load on a recovering service, potentially causing it to crash again.
To prevent this, we need a smarter delay strategy that gives the service breathing room and spreads out the retry attempts from clients.
3. The Solution: Exponential Backoff with Jitter
The industry-standard solution to the retry storm problem has two parts: exponential backoff and jitter.
- Exponential Backoff: Instead of a fixed delay, the wait time increases exponentially after each failed attempt. For example, you might wait 1 second after the first failure, 2 seconds after the second, 4 seconds after the third, and so on. This gives a struggling service progressively more time to recover.
- Jitter: To prevent clients from retrying in synchronized waves even with exponential backoff, we add a small, random amount of time to each delay. This "jitter" spreads out the retry attempts, smoothing the load on the service.
Let's return to the same video to see how these concepts are explained and visualized.
Retries & Exponential Backoff - Deep Dive
Continuing with "Retries & Exponential Backoff - Deep Dive."
Watch these two segments to understand the solution: Exponential backoff: This part explains the formula for calculating an exponentially increasing delay. Adding jitter: Here, the speaker references a famous AWS architecture blog post to explain why backoff alone isn't enough and how adding randomization (jitter) further improves system stability by desynchronizing clients.
The AWS blog post mentioned in the video includes a powerful graph that demonstrates the effectiveness of adding jitter.

This graph from an AWS article compares the total work (number of calls) performed by different retry strategies under increasing client load. The "None" line (no backoff) shows a massive increase in work. "Exponential" backoff is better, but the strategies with jitter ("EqualJitter," "FullJitter") perform the best, keeping the system load much lower.
The takeaway is clear: randomized exponential backoff is the most effective strategy for building resilient clients.
4. Practical Implementation in Python
While you could implement this logic from scratch, it's a solved problem. For production code, you should use a battle-tested library. In the Python ecosystem, Tenacity is the go-to choice for robust retry logic. It's clean, powerful, and integrates easily using decorators.
The following video walks through building retry logic from first principles and then recommends using a library like Tenacity for real-world use.
Retry Pattern: The Secret to Resilient Python Code
The video "Retry Pattern: The Secret to Resilient Python Code" from ArjanCodes provides a great, practical walkthrough.
Watch these two clips: Adding exponential backoff: See how the simple retry function is modified to include an exponentially increasing delay. Using Tenacity: The speaker rightly points out that you shouldn't reinvent this wheel and introduces the Tenacity library.
Now, let's put this into practice. The documentation from BytePlus (the provider of ModelArk) gives a perfect, concise code example for using Tenacity to call an LLM API.
Best practices for handling burst traffic--ModelArk-Byteplus
This document provides best practices for handling traffic spikes, with a specific section on implementing a client-side retry strategy.
Focus on the section "Practice 3: backoff and retry strategy". Study the Python code example that uses the @retry decorator from Tenacity. Notice the wait_random_exponential(min=1, max=60) argument—this single function implements the entire exponential backoff with jitter strategy.
Your Task: Implement a Robust LLM Client
Using the concepts and examples above, your task is to write a small Python script that demonstrates a resilient API call.
- Make sure you have
tenacityinstalled (pip install tenacity). - Write a function
call_llm_api()that simulates making a network call. Inside this function, use Python'srandommodule to randomly raise aConnectionError(simulating a transient failure) about 70% of the time. - Decorate this function with Tenacity's
@retrydecorator. Configure it to usewait_random_exponentialand tostop_after_attempt(5). - Call your decorated function inside a
try...exceptblock and print the outcome, whether it eventually succeeded or failed after all retries.
Here is a template to get you started:
import random
import time
from tenacity import retry, stop_after_attempt, wait_random_exponential, RetryError
# Simulate an LLM API client that is unreliable
def call_llm_api(prompt: str):
"""Simulates a call to an LLM API that might fail transiently."""
print("Attempting to call the LLM API...")
# Simulate a 70% chance of a transient failure
if random.random() < 0.7:
print("API call failed: Server temporarily unavailable.")
# Raise a transient, retryable error
raise ConnectionError("Server temporarily unavailable")
print("API call successful!")
return f"Response for: '{prompt}'"
# Your goal is to apply the retry decorator here
# @retry(...)
def robust_llm_call(prompt: str):
"""
A wrapper around the API call that automatically handles retries.
"""
return call_llm_api(prompt)
# --- Main execution ---
if __name__ == "__main__":
try:
print("Making a robust call to the LLM...")
response = robust_llm_call("Tell me a short story.")
print("\n--- Success! ---")
print(f"Final response received:\n{response}")
except RetryError as e:
print("\n--- Failure ---")
print(f"Failed to get response after multiple retries. The final attempt failed with: {e}")
Running this script multiple times will show the retry mechanism in action, sometimes succeeding after a few tries and sometimes failing after hitting the attempt limit.
Conclusion
You have now learned how to design and implement one of the most fundamental reliability patterns for any distributed system. By building clients that can intelligently retry transient failures, you significantly improve the resilience and user experience of your application.
Key Takeaways:
- Retry on Transient Errors Only: Differentiate between temporary failures (like
429,503, network errors) and permanent ones (400,401). - Avoid Retry Storms: Simple, fixed-delay retries can overwhelm a recovering service.
- Use Exponential Backoff with Jitter: This is the standard pattern to give services time to recover and to de-synchronize client retries, preventing "thundering herds."
- Don't Reinvent the Wheel: For production systems, use robust libraries like Python's
tenacityto implement retry logic.
So far in this module, we've covered scaling, queuing, and now client-side retries. We're building a solid foundation in service reliability. In the next lesson, we will move back to the server-side to explore a crucial topic for stateful services like LLMs: analyzing the trade-offs of different load balancing strategies for stateful (KV cache) vs. stateless LLM replicas.