Rate limits will kill your agent at scale. Here's how to design around them.

One user, one agent, no rate limit issues. Ten concurrent users, the agent calls your upstream APIs ten times faster. At fifty users, you're throttled, retrying, and burning context on error handling.

B

Balagei G Nagarajan

7 MIN READ


Your agent makes three LLM calls and five tool calls per user request. At one concurrent user, that's eight calls total. At fifty concurrent users, that's four hundred calls hitting your upstream APIs simultaneously.

Most LLM providers rate limit by requests per minute and tokens per minute. Most external APIs rate limit by requests per second or requests per minute. At fifty concurrent users, your agent's call rate is fifty times higher than at one user — but your rate limits haven't changed.

You will hit the limit. The question is whether your agent is designed to handle it gracefully.

When a 429 (rate limit exceeded) response arrives, back off before retrying.
— from "Rate limits will kill your agent at scale. Here's how to design around them."

Two kinds of rate limits to manage

LLM provider limits (RPM + TPM). Your LLM provider limits how many requests and tokens you can consume per minute. An agent that makes three LLM calls per user request consumes three times the RPM quota of a single-call system. At twenty concurrent users making three calls each, that's sixty RPM. At a 100 RPM limit, you have headroom. At sixty concurrent users, you're over.

Token limits are sneakier. An agent accumulating context over a long session uses more tokens per call as the session grows. Token consumption is not linear — it grows with session length. Plan for the worst-case token consumption, not the average.

Upstream tool API limits. Every external API your agent calls has its own rate limit. These limits are set for the API's total traffic across all your users, not per-user. When thirty concurrent agent sessions each call get_user_record simultaneously, that's thirty calls to your user service in a tight burst. If the service rate limits at twenty requests per second, you get throttled.

The patterns

Token bucket at the agent gateway. Before any agent session makes calls to external services, it requests tokens from a token bucket that governs the total call rate. The bucket refills at the maximum sustainable rate. Sessions that arrive when the bucket is empty wait rather than proceed.

This pattern is the standard for preventing thundering herd: many concurrent sessions trying to call the same API simultaneously. The token bucket smooths the burst into a steady flow.

class AgentGateway:
    def __init__(self, max_calls_per_second: float):
        self.bucket = TokenBucket(capacity=max_calls_per_second * 10, 
                                   refill_rate=max_calls_per_second)
    
    async def call_tool(self, tool: str, args: dict) -> dict:
        await self.bucket.acquire()  # Blocks if rate limit reached
        return await tools[tool].call(args)

Per-tool rate limiters. Different tools have different limits. A search API might allow 100 calls per minute. A payment API might allow 10. Implement a separate rate limiter for each tool, configured with that tool's actual limit.

Exponential backoff with jitter on 429 responses. When a 429 (rate limit exceeded) response arrives, back off before retrying. Use exponential backoff (double the wait time on each retry) with jitter (add randomness to the wait time). The jitter prevents all waiting sessions from retrying simultaneously when the rate limit window resets.

async def call_with_backoff(tool: str, args: dict, max_retries: int = 3) -> dict:
    for attempt in range(max_retries):
        try:
            return await call_tool(tool, args)
        except RateLimitError as e:
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) + random.uniform(0, 1)  # Exponential + jitter
            await asyncio.sleep(wait)

LLM call batching and caching. For tool calls that return the same result for the same input (user record lookup, product catalog, etc.), cache the result within the session and across sessions with a TTL. If ten concurrent sessions all need the same user record, make one API call and share the result.

For LLM calls, batch similar requests where possible. If multiple low-priority tasks can be processed together, batch them into a single LLM call rather than making ten sequential calls.

Queue with priority and rate shaping. For high-load systems, move the agent's calls through a work queue. The queue accepts calls at any rate but dispatches them to upstream services at the rate those services allow. Priority queuing ensures that high-priority sessions (paying customers, SLA-bound users) get dispatched first.

Designing for your actual limits

Before you deploy, document the rate limits for every upstream dependency your agent calls:

Service Limit Agent calls per user request Max concurrent users
LLM provider 100 RPM 3 33
User service 50 RPS 1 50
Payment API 10 RPM 1 10
Search API 200 RPM 2 100

The binding constraint is your lowest-capacity upstream relative to your call multiplier. In the example above, the payment API's 10 RPM limit, combined with one call per user request, caps you at ten concurrent users before you hit rate limits.

Design your backpressure architecture around the binding constraint. If the payment API is the bottleneck, implement a payment API-specific queue with priority handling. Don't design for the average limit — design for the minimum.

Test under load before you ship. Run your agent at 2x your expected concurrent user count. Measure which upstream services get throttled first. Verify that your backpressure implementation actually throttles requests rather than cascading failures to users. Fix the gaps before production.

The agent that works for one user and fails for fifty didn't fail because of the agent. It failed because no one modeled the call amplification at scale. Model it now.


Share this post

Join the discussion

Have a take, a war story, or a question? Sign in with GitHub to comment and react. Comments are powered by GitHub Discussions, ad-free and yours to moderate.

Continue Reading

Find where your agent breaks, before you build it

Faultmap maps where your agent will fail from the goal and your data, then hands you the first test suite it has to pass.