← Back to DevBytes

Rate Limiting and Retry Strategies with AutoGen: Complete Guide

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.

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles