← Back to DevBytes

AI Agent Rate Limiting: Protecting Your LLM Budget

Understanding Rate Limiting for AI Agents

When you build an AI agent that calls large language models (LLMs) through paid APIs, every request consumes tokens and burns through your budget. Without safeguards, a runaway agent loop, a bug, or an unexpected spike in traffic can generate thousands of dollars in charges within hours. Rate limiting is the primary defense mechanism that protects your LLM budget by controlling how many requests your agent is allowed to make over time.

What Is AI Agent Rate Limiting?

Rate limiting is a technique that restricts the number of operations an agent can perform within a given time window. In the context of LLM-powered agents, it means capping the frequency and sometimes the cumulative cost of API calls. Instead of letting every prompt fly to the provider immediately, the agent checks whether it is within its allowed quota before sending a request. If the limit has been reached, the agent either waits, queues the request, or fails gracefully.

Why It Matters: Protecting Your LLM Budget

LLM APIs charge by token, and costs can scale dramatically with complex agent workflows. A single agentic task might involve dozens of model invocations for planning, reasoning, tool use, and verification. Without rate limiting, you risk:

Rate limiting transforms your agent from an unchecked spender into a predictable, cost-controlled component. It aligns the agent's behaviour with the financial boundaries you set, ensuring that innovation doesn't accidentally bankrupt the project.

Core Rate Limiting Algorithms

Several well-known algorithms can be adapted for LLM budget protection. The choice depends on whether you care about precise smoothness, memory footprint, or burst handling.

Token Bucket

The token bucket is the most popular algorithm for API rate limiting. Imagine a bucket that holds a fixed number of tokens. Each token represents permission to make one request. Tokens are added at a steady refill rate (e.g., one token every second). When a request arrives, the agent tries to remove a token from the bucket. If the bucket is empty, the request is blocked or delayed until a token arrives. This allows short bursts while maintaining a long-term average rate.

Here is a complete Python implementation suitable for an AI agent:


import time
import threading

class TokenBucket:
    def __init__(self, rate, capacity):
        """
        rate: Number of tokens added per second (sustained rate).
        capacity: Maximum number of tokens the bucket can hold (burst size).
        """
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity  # start full
        self.last_refill = time.monotonic()
        self.lock = threading.Lock()

    def _refill(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        # Refill based on elapsed time
        refill_amount = elapsed * self.rate
        self.tokens = min(self.capacity, self.tokens + refill_amount)
        self.last_refill = now

    def consume(self, tokens_needed=1):
        """
        Try to consume `tokens_needed` tokens.
        Returns True if allowed, False otherwise.
        """
        with self.lock:
            self._refill()
            if self.tokens >= tokens_needed:
                self.tokens -= tokens_needed
                return True
            return False

    def wait_and_consume(self, tokens_needed=1, max_wait=None):
        """
        Block until tokens become available, or return False if max_wait exceeded.
        """
        wait_start = time.monotonic()
        while True:
            with self.lock:
                self._refill()
                if self.tokens >= tokens_needed:
                    self.tokens -= tokens_needed
                    return True
            # Sleep a short time to allow refill; check max_wait
            if max_wait and (time.monotonic() - wait_start) > max_wait:
                return False
            time.sleep(0.1)

Usage in an agent loop: create a bucket with a rate of 1 request per second and capacity of 10 for bursts. Before calling the LLM, call bucket.consume(). If it returns False, log a warning and skip the call or fall back to a cached response.

Sliding Window Log

This algorithm tracks the exact timestamps of recent requests and allows a fixed number of requests within a sliding window (e.g., 100 requests in the last 60 seconds). It provides precise control but uses more memory. Perfect for strict per-minute budget enforcement.


from collections import deque
import time
import threading

class SlidingWindowRateLimiter:
    def __init__(self, window_seconds, max_requests):
        self.window_seconds = window_seconds
        self.max_requests = max_requests
        self.request_times = deque()
        self.lock = threading.Lock()

    def is_allowed(self):
        now = time.monotonic()
        with self.lock:
            # Remove timestamps outside the window
            while self.request_times and self.request_times[0] < now - self.window_seconds:
                self.request_times.popleft()
            if len(self.request_times) < self.max_requests:
                self.request_times.append(now)
                return True
            return False

This is ideal when you need to respect a provider’s hard limit like “60 requests per minute” exactly, without the burst flexibility of a token bucket.

Fixed Window Counter

A simpler alternative divides time into fixed windows (e.g., one-minute buckets) and counts requests per window. It resets the counter at each boundary. It's easy to implement but suffers from the “burst at boundary” problem where two windows’ worth of requests can happen back-to-back. Use it only if memory is extremely constrained and precision isn’t critical.

Integrating Rate Limiting into an AI Agent

Rate limiting must sit between the agent’s decision logic and the actual LLM API call. The most maintainable approach is to wrap the LLM client or the function responsible for calling the model.

Wrapping LLM Calls

Create a decorator or a wrapper class that applies rate limiting transparently. This keeps the agent code clean and centralises budget protection.


import functools
from my_rate_limiters import TokenBucket

# Global bucket shared across all agent calls
llm_bucket = TokenBucket(rate=2, capacity=5)  # 2 requests/sec, burst of 5

def rate_limited_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if llm_bucket.consume():
            return func(*args, **kwargs)
        else:
            # Fallback behaviour: return a cached response, raise an exception, or wait
            raise RuntimeError("Rate limit exceeded – request blocked by budget guard.")
    return wrapper

# Apply to the function that actually sends prompts to the LLM
@rate_limited_call
def complete_prompt(prompt):
    # Actual API call here
    return llm_client.complete(prompt)

Alternatively, use a context manager for block-level control:


class RateLimitGuard:
    def __init__(self, bucket, fallback_response=None):
        self.bucket = bucket
        self.fallback = fallback_response

    def __enter__(self):
        if not self.bucket.consume():
            raise RuntimeError("Rate limit hit")

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

# Usage
try:
    with RateLimitGuard(llm_bucket):
        response = llm_client.complete(prompt)
except RuntimeError:
    response = "Agent paused – budget protection active."

Budget-Aware Rate Limiting

Pure rate limiting controls call frequency, but it doesn’t track how much money you’ve actually spent. To truly protect your LLM budget, you need a budget tracker that monitors cumulative cost and stops the agent when a daily or monthly cap is reached. Combine this with rate limiting for layered defence.


import time

class BudgetTracker:
    def __init__(self, max_budget_usd, cost_per_token_model):
        self.max_budget_usd = max_budget_usd
        self.cost_per_token = cost_per_token_model  # dict mapping model name to $/token
        self.spent = 0.0
        self.overdraft = False
        self.lock = threading.Lock()

    def record_usage(self, model_name, tokens_used):
        cost = tokens_used * self.cost_per_token.get(model_name, 0.0001)
        with self.lock:
            self.spent += cost
            if self.spent >= self.max_budget_usd:
                self.overdraft = True

    def is_budget_exceeded(self):
        with self.lock:
            return self.overdraft

# Example integration with a token bucket
class BudgetAwareAgent:
    def __init__(self, bucket, budget_tracker):
        self.bucket = bucket
        self.budget_tracker = budget_tracker

    def call_llm(self, model, prompt, max_tokens):
        if self.budget_tracker.is_budget_exceeded():
            raise Exception("Monthly budget exhausted – all LLM calls blocked.")
        if not self.bucket.consume():
            raise Exception("Rate limit – try again later.")
        response = llm_client.complete(model, prompt, max_tokens)
        # After successful call, record cost
        tokens_used = response.usage.total_tokens
        self.budget_tracker.record_usage(model, tokens_used)
        return response

This dual-guard pattern ensures that even if the rate limiter allows a request, the budget tracker can still veto it. It’s the closest you can get to an automated spending kill-switch.

Best Practices

Conclusion

Rate limiting for AI agents is not an optional optimisation—it’s a fundamental safety net that keeps your LLM budget intact. By wrapping every model invocation with a token bucket or sliding window limiter, and layering on budget tracking, you transform an unpredictable agent into a financially predictable service. The techniques shown here can be dropped into any Python agent framework with minimal changes. Start with a simple token bucket, add cost tracking, and you’ll sleep better knowing that your agent won’t accidentally spend your entire cloud compute budget on a single runaway loop.

— Ad —

Google AdSense will appear here after approval

← Back to all articles