Introduction/ Issue:
When building LLM applications, we usually focus on prompts, agents, and workflows. However, one challenge that often gets overlooked is handling temporary API failures. Even if our code is correct, the LLM provider may occasionally return errors such as rate limits (429), request timeouts, or temporary service outages.
In many projects, these temporary failures cause the entire application or workflow to fail, even though simply retrying the request after a short delay would have solved the problem. As developers, we often end up writing the same retry logic repeatedly for different projects.
To avoid this repetitive work and make AI applications more reliable, I created a reusable retry wrapper that automatically retries failed LLM requests using exponential backoff and jitter. Since it is built on LangChain’s BaseChatModel, the same component can be used with different LLM providers without changing the retry logic.
Why we need to do / Cause of the issue:
Temporary API failures are a normal part of working with cloud-based AI services. A request may fail because the provider is experiencing high traffic, the network connection is briefly interrupted, or the application has exceeded the API rate limit.
Some common errors include:
- Rate limit errors (HTTP 429)
- Request timeouts
- Temporary connection failures
- Service unavailable (HTTP 503)
- Gateway timeout (HTTP 504)
The important thing is that these errors are usually temporary. In most cases, waiting a few seconds and sending the same request again is enough for it to succeed.
Without a retry mechanism, the impact can be significant:
- AI workflows stop unexpectedly.
- Long-running document processing jobs fail midway.
- Users have to retry operations manually.
- The same retry logic gets implemented repeatedly across different projects.
A reusable retry component solves these problems by handling temporary failures automatically. Instead of failing immediately, it waits for a short period, retries the request, and only reports an error if all retry attempts are exhausted.
To make retries more efficient, the component uses exponential backoff, where the waiting time increases after each failed attempt, along with a small amount of random jitter to prevent multiple requests from retrying at the exact same moment.
How do we solve:
To make our LLM applications more reliable, we’ll create a reusable retry wrapper that can be used with any LangChain chat model. Once created, this component can simply be imported into future projects instead of rewriting the retry logic every time.
Follow the steps below to get started.
Step 1: Install the Required Libraries
Install the required dependencies using pip:
pip install langchain langchain-core langchain-groq python-dotenv
Step 2: Store Your API Key
Create a .env file in your project directory and store your API key securely.

Step 3: Add the Reusable Retry Wrapper
Create a new Python file named ai_retry_wrapper.py and paste the reusable retry wrapper code provided in this blog.
from __future__ import annotations
import random
import time
from typing import Any, Optional
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import BaseMessage, HumanMessage
class RetryExhaustedError(Exception):
“””
Raised when the LLM call still fails after all retry attempts.
The original exception is stored in `last_exception`
so applications can inspect the root cause if needed.
“””
def __init__(self, message: str, last_exception: Exception):
super().__init__(message)
self.last_exception = last_exception
# Common error keywords returned by most LLM providers.
# Instead of importing provider-specific exception classes,
# we detect retryable failures using these markers.
RETRYABLE_MARKERS = (
“rate limit”,
“429”,
“timeout”,
“timed out”,
“connection”,
“temporarily unavailable”,
“service unavailable”,
“502”,
“503”,
“504”,
“overloaded”,
)
def _is_retryable(error: Exception) -> bool:
“””
Determines whether an exception should be retried.
Parameters
———-
error : Exception
Exception raised by the LLM provider.
Returns
——-
bool
True if the exception represents a temporary failure.
“””
message = str(error).lower()
return any(marker in message for marker in RETRYABLE_MARKERS)
class RetryWrapper:
“””
A reusable wrapper that adds automatic retry capability to
any LangChain chat model.
Features
——–
– Automatic retries
– Exponential backoff
– Random jitter
– Supports synchronous and asynchronous calls
Compatible with:
– ChatGroq
– ChatOpenAI
– ChatAnthropic
– AzureChatOpenAI
– Any BaseChatModel implementation
“””
def __init__(
self,
llm: BaseChatModel,
max_retries: int = 4,
base_delay: float = 1.0,
max_delay: float = 30.0,
jitter: float = 0.5,
verbose: bool = False,
):
“””
Initializes the retry wrapper.
Parameters
———-
llm : BaseChatModel
LangChain chat model instance.
max_retries : int
Maximum retry attempts before giving up.
base_delay : float
Initial waiting time before the first retry.
max_delay : float
Maximum delay allowed during exponential backoff.
jitter : float
Random delay added to prevent simultaneous retries.
verbose : bool
Prints retry logs when enabled.
“””
self.llm = llm
self.max_retries = max_retries
self.base_delay = base_delay
self.max_delay = max_delay
self.jitter = jitter
self.verbose = verbose
def _log(self, message: str) -> None:
“””
Prints retry information when verbose mode is enabled.
“””
if self.verbose:
print(f”[RetryWrapper] {message}”)
def _backoff_delay(self, attempt: int) -> float:
“””
Calculates exponential backoff delay.
Formula:
base_delay × 2^(attempt-1)
The delay is capped using max_delay and a small random
jitter is added to avoid retry spikes.
“””
delay = min(
self.base_delay * (2 ** (attempt – 1)),
self.max_delay,
)
delay += random.uniform(0, self.jitter)
return delay
def invoke(
self,
prompt: str | list[BaseMessage],
**kwargs: Any,
) -> BaseMessage:
“””
Sends a synchronous request to the LLM with retry support.
Parameters
———-
prompt : str | list[BaseMessage]
Either a plain prompt string or LangChain messages.
Returns
——-
BaseMessage
Response returned by the LLM.
“””
# Convert plain strings into LangChain HumanMessage objects.
messages = (
[HumanMessage(content=prompt)]
if isinstance(prompt, str)
else prompt
)
last_exception: Optional[Exception] = None
for attempt in range(1, self.max_retries + 1):
try:
self._log(f”Attempt {attempt}/{self.max_retries}”)
return self.llm.invoke(messages, **kwargs)
except Exception as error:
last_exception = error
# Validation errors or authentication issues
# should fail immediately instead of retrying.
if not _is_retryable(error):
self._log(
f”Non-retryable error: {error}”
)
raise
if attempt == self.max_retries:
self._log(
f”Final retry failed: {error}”
)
break
delay = self._backoff_delay(attempt)
self._log(
f”Retryable error detected.”
f” Retrying in {delay:.2f} seconds…”
)
time.sleep(delay)
raise RetryExhaustedError(
f”LLM call failed after {self.max_retries} attempts.”,
last_exception,
)
async def ainvoke(
self,
prompt: str | list[BaseMessage],
**kwargs: Any,
) -> BaseMessage:
“””
Asynchronous version of invoke().
Uses the same retry strategy while awaiting the
underlying async LangChain model.
“””
import asyncio
messages = (
[HumanMessage(content=prompt)]
if isinstance(prompt, str)
else prompt
)
last_exception: Optional[Exception] = None
for attempt in range(1, self.max_retries + 1):
try:
self._log(
f”Async attempt {attempt}/{self.max_retries}”
)
return await self.llm.ainvoke(
messages,
**kwargs,
)
except Exception as error:
last_exception = error
if not _is_retryable(error):
self._log(
f”Non-retryable error: {error}”
)
raise
if attempt == self.max_retries:
self._log(
f”Final retry failed: {error}”
)
break
delay = self._backoff_delay(attempt)
self._log(
f”Retryable error detected.”
f” Retrying in {delay:.2f} seconds…”
)
await asyncio.sleep(delay)
raise RetryExhaustedError(
f”LLM call failed after {self.max_retries} attempts.”,
last_exception,
)
if __name__ == “__main__”:
import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq
# Load environment variables from .env
load_dotenv()
llm = ChatGroq(
model=”llama-3.3-70b-versatile”,
api_key=os.getenv(“GROQ_API_KEY”),
temperature=0,
)
wrapper = RetryWrapper(
llm=llm,
max_retries=4,
base_delay=1,
verbose=True,
)
response = wrapper.invoke(
“Explain why retries are important in AI applications in one sentence.”
)
print(“\nLLM Response:”)
print(response.content)
Step 4: Run the Component
Execute the file to verify that the wrapper is working correctly.
python ai_retry_wrapper.py

Conclusion:
Building an LLM application is not just about generating responses—it is also about making the application reliable enough to handle real-world situations. Temporary API failures are unavoidable, but they should not interrupt the user experience or bring an entire workflow to a halt.
By creating a reusable retry wrapper, we move all retry logic into a single, reusable component that can be plugged into any LangChain-based application. This reduces code duplication, follows industry best practices with exponential backoff and jitter, and makes AI applications more resilient and easier to maintain.
Instead of writing retry logic for every new project, we can simply reuse this component and focus on building better AI solutions, knowing that temporary failures are handled automatically in the background.