Rate Limiting and Retry Strategies with AutoGen: Complete Guide
When building multi-agent applications with Microsoft's AutoGen framework, your agents will inevitably make numerous calls to LLM APIs like OpenAI, Anthropic, or Azure OpenAI. These APIs enforce strict rate limits, and network conditions can cause transient failures. Without proper rate limiting and retry strategies, your multi-agent workflows will crash unpredictably, waste tokens, and deliver a poor user experience. This guide walks you through everything you need to know to build resilient AutoGen applications that handle API constraints gracefully.
What Is Rate Limiting and Why It Matters in AutoGen
Rate limiting is the practice of controlling the frequency and volume of requests sent to an API. LLM providers impose limits such as requests per minute (RPM), tokens per minute (TPM), and concurrent requests. When you exceed these thresholds, the API returns a 429 Too Many Requests status code.
In AutoGen, the problem is amplified because multiple agents often converse in tight loops. A group chat with five agents can easily generate dozens of API calls in seconds. Without rate limiting, you will hit API ceilings quickly. Without retry logic, a single 429 or 503 response can terminate an entire conversation that took minutes to build up.
- Rate limiting prevents you from exceeding API quotas proactively.
- Retry strategies recover from failures reactively with intelligent backoff.
- Together, they ensure your AutoGen workflows are production-ready.
Understanding AutoGen's Built-in Retry Mechanism
AutoGen's OpenAIWrapper class includes a built-in retry mechanism that handles transient errors automatically. By default, it retries on rate limit errors and connection errors using exponential backoff. Let's look at how to configure it.
import autogen
config_list = [
{
"model": "gpt-4",
"api_key": "your-api-key",
}
]
llm_config = {
"config_list": config_list,
"timeout": 60,
"max_retries": 5,
"wait_interval": 2,
"wait_interval_max": 60,
"retry_wait_min": 5,
"retry_wait_max": 120,
}
assistant = autogen.AssistantAgent(
name="assistant",
llm_config=llm_config,
)
Here is what each parameter controls:
timeout: Maximum seconds to wait for a single API response.max_retries: Maximum number of retry attempts before giving up.wait_interval: Base interval in seconds between retries.wait_interval_max: Upper cap on the wait interval.retry_wait_min: Minimum random jitter added to wait time.retry_wait_max: Maximum random jitter added to wait time.
The built-in mechanism uses exponential backoff with jitter, meaning each retry waits longer than the previous one, with a random component to avoid thundering herd problems when multiple agents retry simultaneously.
Implementing Custom Rate Limiting with Token Buckets
For fine-grained control, you can implement a token bucket rate limiter that caps requests per minute. This is especially useful when running multiple AutoGen agents concurrently or when you share an API key across services.
import time
import threading
from collections import deque
class TokenBucketRateLimiter:
def __init__(self, requests_per_minute, tokens_per_minute=None):
self.min_interval = 60.0 / requests_per_minute
self.tokens_per_minute = tokens_per_minute
self.last_request_time = 0
self.token_usage_window = deque()
self.lock = threading.Lock()
def wait_if_needed(self, estimated_tokens=0):
with self.lock:
now = time.time()
elapsed = now - self.last_request_time
if elapsed < self.min_interval:
sleep_time = self.min_interval - elapsed
time.sleep(sleep_time)
now = time.time()
if self.tokens_per_minute and estimated_tokens > 0:
cutoff = now - 60
while self.token_usage_window and self.token_usage_window[0][0] < cutoff:
self.token_usage_window.popleft()
current_tokens = sum(t for _, t in self.token_usage_window)
if current_tokens + estimated_tokens > self.tokens_per_minute:
wait = 60 - (now - self.token_usage_window[0][0])
if wait > 0:
time.sleep(wait)
self.last_request_time = time.time()
if self.tokens_per_minute and estimated_tokens > 0:
self.token_usage_window.append((self.last_request_time, estimated_tokens))
rate_limiter = TokenBucketRateLimiter(
requests_per_minute=50,
tokens_per_minute=90000
)
You can integrate this limiter into a custom agent by overriding the generate_reply method:
class RateLimitedAssistantAgent(autogen.AssistantAgent):
def __init__(self, *args, rate_limiter=None, **kwargs):
super().__init__(*args, **kwargs)
self.rate_limiter = rate_limiter
def generate_reply(self, messages=None, sender=None, **kwargs):
estimated_tokens = sum(len(m.get("content", "").split()) for m in (messages or [])) * 2
if self.rate_limiter:
self.rate_limiter.wait_if_needed(estimated_tokens)
return super().generate_reply(messages=messages, sender=sender, **kwargs)
assistant = RateLimitedAssistantAgent(
name="rate_limited_assistant",
llm_config=llm_config,
rate_limiter=rate_limiter,
)
Building a Robust Retry Wrapper
While AutoGen's built-in retry is sufficient for most cases, you may want a custom retry wrapper for scenarios involving custom API endpoints, streaming responses, or specialized error handling. The following implementation uses exponential backoff with jitter and classifies which errors are retryable.
import random
import logging
from functools import wraps
logger = logging.getLogger(__name__)
RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
def is_retryable_error(error):
if hasattr(error, "status_code"):
return error.status_code in RETRYABLE_STATUS_CODES
error_str = str(error).lower()
return any(keyword in error_str for keyword in [
"timeout", "connection", "rate limit", "overloaded"
])
def retry_with_backoff(
max_retries=5,
base_delay=1.0,
max_delay=120.0,
backoff_factor=2.0,
jitter=True
):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
if attempt == max_retries or not is_retryable_error(e):
raise
delay = min(base_delay * (backoff_factor ** attempt), max_delay)
if jitter:
delay = delay * (0.5 + random.random() * 0.5)
logger.warning(
f"Attempt {attempt + 1}/{max_retries} failed: {e}. "
f"Retrying in {delay:.2f}s..."
)
time.sleep(delay)
raise last_exception
return wrapper
return decorator
You can apply this decorator to custom functions that AutoGen agents call, such as tool functions or external API integrations:
@retry_with_backoff(max_retries=4, base_delay=2.0)
def fetch_stock_price(symbol):
import requests
response = requests.get(f"https://api.example.com/stocks/{symbol}")
response.raise_for_status()
return response.json()
@autogen.register_for_execution()
@autogen.register_for_llm(description="Get the current stock price for a symbol")
def get_stock_price(symbol: str) -> str:
data = fetch_stock_price(symbol)
return f"The current price of {symbol} is ${data['price']}"
Handling Rate Limits in Group Chat Scenarios
Group chats are the most rate-limit-prone pattern in AutoGen because multiple agents respond in sequence, often rapidly. The key strategies are to use a shared rate limiter across all agents and to configure the group chat manager to handle pauses gracefully.
shared_limiter = TokenBucketRateLimiter(
requests_per_minute=40,
tokens_per_minute=80000
)
agent_a = RateLimitedAssistantAgent(
name="researcher",
system_message="You are a research analyst.",
llm_config=llm_config,
rate_limiter=shared_limiter,
)
agent_b = RateLimitedAssistantAgent(
name="writer",
system_message="You are a content writer.",
llm_config=llm_config,
rate_limiter=shared_limiter,
)
agent_c = RateLimitedAssistantAgent(
name="reviewer",
system_message="You are a quality reviewer.",
llm_config=llm_config,
rate_limiter=shared_limiter,
)
user_proxy = autogen.UserProxyAgent(
name="user",
human_input_mode="NEVER",
max_consecutive_auto_reply=10,
)
groupchat = autogen.GroupChat(
agents=[user_proxy, agent_a, agent_b, agent_c],
messages=[],
max_round=15,
)
manager = autogen.GroupChatManager(
groupchat=groupchat,
llm_config=llm_config,
)
user_proxy.initiate_chat(manager, message="Write a report about renewable energy trends.")
By sharing a single TokenBucketRateLimiter instance across all agents, you ensure that the combined request rate stays within your API quota regardless of how many agents participate.
Using Async AutoGen with Rate Limiting
For high-throughput applications, AutoGen's async API allows concurrent agent execution. However, concurrency makes rate limiting even more critical. Here is how to combine async agents with an async-compatible rate limiter.
import asyncio
import autogen
from autogen import AssistantAgent, UserProxyAgent
class AsyncRateLimiter:
def __init__(self, requests_per_minute):
self.min_interval = 60.0 / requests_per_minute
self.last_request = 0.0
self.lock = asyncio.Lock()
async def acquire(self):
async with self.lock:
now = asyncio.get_event_loop().time()
wait = self.min_interval - (now - self.last_request)
if wait > 0:
await asyncio.sleep(wait)
self.last_request = asyncio.get_event_loop().time()
async_limiter = AsyncRateLimiter(requests_per_minute=30)
async def run_agent_task(agent, user_proxy, message):
await async_limiter.acquire()
await user_proxy.a_initiate_chat(agent, message=message)
async def main():
config_list = [{"model": "gpt-4", "api_key": "your-key"}]
llm_config = {"config_list": config_list, "max_retries": 5}
tasks = []
for i in range(5):
agent = AssistantAgent(
name=f"agent_{i}",
llm_config=llm_config,
system_message="You are a helpful assistant.",
)
user_proxy = UserProxyAgent(
name=f"user_{i}",
human_input_mode="NEVER",
max_consecutive_auto_reply=2,
)
tasks.append(run_agent_task(agent, user_proxy, f"Summarize topic {i}"))
await asyncio.gather(*tasks)
asyncio.run(main())
Monitoring and Observability
Rate limiting and retries are only effective if you can observe their behavior. Implement logging to track retry attempts, rate limit hits, and wait times. This data helps you tune your parameters over time.
import logging
import time
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger("autogen.ratelimit")
class ObservableRateLimiter(TokenBucketRateLimiter):
def wait_if_needed(self, estimated_tokens=0):
start = time.time()
super().wait_if_needed(estimated_tokens)
wait_duration = time.time() - start
if wait_duration > 0.1:
logger.info(
f"Rate limiter paused execution for {wait_duration:.2f}s "
f"(estimated_tokens={estimated_tokens})"
)
class RetryMetrics:
def __init__(self):
self.total_retries = 0
self.retries_by_status = {}
self.total_wait_time = 0.0
def record_retry(self, status_code, wait_time):
self.total_retries += 1
self.retries_by_status[status_code] = self.retries_by_status.get(status_code, 0) + 1
self.total_wait_time += wait_time
logger.info(
f"Retry recorded: status={status_code}, wait={wait_time:.2f}s, "
f"total_retries={self.total_retries}"
)
def summary(self):
return {
"total_retries": self.total_retries,
"retries_by_status": self.retries_by_status,
"total_wait_time": round(self.total_wait_time, 2),
}
metrics = RetryMetrics()
Best Practices
- Set conservative RPM limits. Use 70-80% of your API tier's maximum RPM to leave headroom for bursts and other services sharing the key.
- Always use exponential backoff with jitter. Fixed delays cause synchronized retry storms when multiple agents fail simultaneously.
- Respect
Retry-Afterheaders. When an API returns a429with aRetry-Afterheader, honor that value instead of computing your own delay. - Share rate limiters across agents. In group chats, a single shared limiter prevents the combined traffic from exceeding quotas.
- Cache responses when possible. AutoGen supports caching via the
cacheparameter inllm_config. Caching eliminates redundant API calls entirely. - Monitor and alert on retry rates. If your retry rate climbs above 5-10% of total requests, your rate limits are too aggressive or your API tier needs upgrading.
- Use smaller models for routine tasks. GPT-3.5 or GPT-4o-mini have higher rate limits than GPT-4. Reserve expensive models for tasks that require them.
- Implement circuit breakers for cascading failures. If retries fail repeatedly, temporarily stop sending requests and alert an operator rather than continuing to hammer a failing endpoint.
- Test under load. Simulate rate limit errors in staging to verify your retry logic works before deploying to production.
Configuring Caching to Reduce API Calls
Caching is the most effective rate limiting strategy because it eliminates requests entirely. AutoGen supports seed-based caching that stores LLM responses so identical prompts do not trigger new API calls.
llm_config_with_cache = {
"config_list": config_list,
"max_retries": 5,
"cache": {
"seed": 42,
"cache_path": ".autogen_cache",
},
}
assistant = autogen.AssistantAgent(
name="cached_assistant",
llm_config=llm_config_with_cache,
)
During development, caching ensures reproducible results and dramatically reduces API costs. In production, use caching for deterministic tasks like classification or extraction where the same input should always produce the same output.
Conclusion
Rate limiting and retry strategies are not optional features in production AutoGen applications — they are foundational requirements. By combining AutoGen's built-in exponential backoff with custom token bucket limiters, shared rate limiters across group chat agents, async-compatible throttling for concurrent workloads, and caching to eliminate redundant calls, you can build multi-agent systems that remain stable and cost-effective even under heavy load. Start with conservative limits, monitor your retry metrics continuously, and adjust parameters based on real-world traffic patterns. With these patterns in place, your AutoGen workflows will gracefully absorb API turbulence and keep delivering results when it matters most.