AI agents

Choosing the Cap Window: Token Bucket, Sliding Log or Fixed

Antonio Evans8 min read

Three columns, one per algorithm, each showing an hour timeline with refused calls in red, two bars beneath for refusals per hour and memory per caller.
Key points

Key points

  1. 1Token bucket stores a token count and a refill timestamp per caller, roughly a few dozen bytes, and tolerates bursts.
  2. 2Sliding log stores one timestamp per allowed call, so memory scales with the limit, but refusals arrive smoothly.
  3. 3Fixed window stores two integers per caller, the cheapest option, but allows up to twice the limit around a window boundary.
  4. 4Match the algorithm to the tool: reads are idempotent and tolerate bursts, writes need smooth, boundary-proof refusal.

Why agents need a cap window per tool, not per account

Tool calling turns a model from a text generator into something that acts. When a user asks for live data, the model emits structured JSON naming a function and its parameters, and your application executes the call against a live API Blaxel's function calling guide. The model proposes; your code disposes. That split is exactly where a rate limiter belongs.

The problem is volume. An agent given one prompt may fan out into dozens or hundreds of calls to LLM providers, vector databases, third-party APIs and internal services, and a recursive loop or a malicious prompt can turn a minor logic error into a cost spike within minutes NeuralTrust's agent throttling guide. A cap applied to the account as a whole protects your bill, but it does not protect the write tool that a single runaway agent is hammering in a tight loop.

So the useful unit is the tool, or the tool-and-caller pair. A coding agent that reads pull request diffs and runs the test suite orchestrates several function calls per review, and each of those functions has different risk Blaxel. Reading a diff twice is harmless. Creating a duplicate ticket twice is not. The cap window should encode that difference, which means choosing a different algorithm for reads and writes rather than a single global bucket.

One more distinction matters before the algorithms. Rate limiting is a hard security and abuse control (requests per second, tool calls per hour), while throttling is a softer fairness and load control (queue depth, latency targets) NeuralTrust. The algorithms below are rate limiters. They refuse. If you want smoothing without refusal, put a queue in front and throttle there.

The three algorithms in one paragraph each

Token bucket. A bucket holds up to N tokens. Tokens refill at a fixed rate. Each call removes one token; if the bucket is empty the call is refused or delayed. Because tokens accumulate while the system is idle, the bucket absorbs a controlled burst up to its capacity, then enforces the average GeeksforGeeks' algorithm reference. AWS API Gateway is a well known implementation of the token bucket approach Zuplo's platform comparison.

Sliding log. You keep a timestamp for every allowed call. On arrival, you drop timestamps older than the window, count what remains, and refuse if the count has hit the limit. Zuplo's primer on the family notes that sliding approaches smooth out the boundary problem at the cost of more computation than a fixed counter Zuplo. The log is the strictest variant: no interpolation, no boundary trick.

Fixed window. Divide time into intervals, count calls in the current interval, and refuse once the counter reaches the limit until the next interval starts Zuplo. It is the cheapest to store and the easiest to explain, and Apigee's Quota policy is built on this fixed window counter model Zuplo.

The failure each one produces, worked with numbers

Take a limit of 60 calls per hour, enforced per caller, and feed all three algorithms the same hostile-but-legal client: a well behaved agent 55 minutes of the hour, then a scripted burst.

Fixed window with hourly boundaries starting at 12:00. The client sends 60 calls at 12:59 and another 60 at 13:01. Both are accepted, because they fall in different windows. The client achieved 120 calls in roughly two minutes against a limit of 60 per hour. Zero refusals. That is the boundary double burst, and it is not a bug in your code, it is the algorithm working as designed Zuplo.

Sliding log with a 60 minute lookback. The 13:01 burst arrives with 60 timestamps still inside the window, so all 60 are refused. The client gets a clean 429 for every attempt and the refusal pattern tracks the real rate. If the client backs off with jitter, retries succeed as timestamps age out, roughly one per minute.

Token bucket, capacity 60, refill 1 token per minute. A client that goes quiet for an hour starts with 60 tokens and can spend all 60 immediately. Then it is refused 59 times over the next 59 minutes before the next token lands. Refusals come in clumps: a burst of permits, then a wall.

Property (limit 60 per hour, per caller)Token bucketSliding logFixed window
State per callerToken count plus refill timestamp (about 16 to 32 bytes)One timestamp per allowed call, up to 60 (about 480 bytes at 8 bytes each, more in Redis)Two integers: window start and counter (about 16 bytes)
Worst case calls in any 120 minute span60 plus one per minute of refill60 per rolling hour, strictly120, if split across the boundary
Refusal pattern under abuseClumped after a burstSmooth, one per expired timestampNone, then a hard cliff at the reset
Cost of a very large limit (say 10,000 per hour)Flat10,000 timestamps per callerFlat
Idempotent read toolGood fitFine but wastefulAcceptable if the limit is coarse
Non-idempotent write toolRisky: a burst of writes lands at onceBest fitAvoid: double burst means double writes

The memory numbers are estimates you should size against your own store, but the ratio is the point. A sliding log's footprint is linear in the limit, and the limit is exactly the number you are most likely to raise later. A token bucket and a fixed window are constant size no matter how generous the cap.

The decision rule: reads get token buckets, writes get logs

For a shared read tool, use a token bucket. Reads are idempotent, so a burst of 60 simultaneous calls is a load problem, not a correctness problem, and the bucket handles it in about 30 bytes of state per caller. The bucket is specifically suited to APIs that need to allow short bursts while holding an average rate over time Zuplo. Your agent's first minute after a long think is bursty by nature, and the bucket rewards that instead of punishing it.

For a write tool, use a sliding log. It stores one timestamp per allowed call, so memory grows with the limit, and that is the price you pay for refusals that are smooth and boundary proof. If the write tool can create, charge, send or delete, a client must never be able to land twice the limit in a two minute span. Refuse to trade that guarantee for bytes.

For everything else, use a fixed window only when the limit is coarse and the action is cheap to repeat or easy to deduplicate. A counter cache that refreshes a public page every 10 minutes at 12 calls per hour can tolerate a boundary double burst. A payment endpoint cannot.

Three secondary rules sharpen the decision:

  1. If the tool has an idempotency key and the downstream honors it, a burst is a load issue not a duplication issue, and the token bucket is safe even for writes.
  2. If the limit is above a few hundred per caller, a sliding log starts to cost real memory at scale, so consider a sliding window counter or accept a token bucket.
  3. If you cannot state the worst case in a sentence, the tool is not ready for a limiter; fix the operation first.

A token bucket for a read tool, in about 20 lines

The whole state is two numbers and a timestamp. The refill is computed lazily on read, which is why the per caller footprint stays flat.

import time

class TokenBucket:
    def __init__(self, capacity: int, refill_per_sec: float):
        self.capacity = capacity
        self.refill_per_sec = refill_per_sec
        self.tokens = float(capacity)   # bytes: 8
        self.updated = time.monotonic() # bytes: 8

    def allow(self, cost: int = 1) -> bool:
        now = time.monotonic()
        elapsed = now - self.updated
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_per_sec)
        self.updated = now
        if self.tokens >= cost:
            self.tokens -= cost
            return True
        return False

# 60 reads per hour, bursts up to 60
bucket = TokenBucket(capacity=60, refill_per_sec=60 / 3600)

Two refinements before production. First, store the bucket in a shared store (Redis with a Lua script is the usual choice) so that replicas do not each grant a full bucket Solos' rate limiting overview. Second, return a Retry-After header on refusal; the token math gives you the exact wait: (cost minus tokens) divided by refill rate.

A sliding log for a write tool, and the cleanup that matters

The log is a sorted set of timestamps. On each call you remove entries older than the window, count the remainder, and refuse at the limit. The subtlety is that you must prune on every check, or memory grows without bound when a caller goes quiet.

import time
from collections import deque

class SlidingLog:
    def __init__(self, limit: int, window_sec: int):
        self.limit = limit
        self.window = window_sec
        self.stamps = deque()  # one float per allowed call

    def allow(self) -> bool:
        now = time.monotonic()
        cutoff = now - self.window
        while self.stamps and self.stamps[0] <= cutoff:
            self.stamps.popleft()
        if len(self.stamps) >= self.limit:
            return False
        self.stamps.append(now)
        return True

The operational cost is visible in the class: stamps grows to limit and no further. A 20 writes per hour cap on a create-ticket tool costs 20 timestamps per caller, about 160 bytes as raw floats, plus the store's overhead. That is cheap for the guarantee, and a per call policy check sits naturally at the boundary before the tool receives arguments, the same shape as an MCP policy proxy that inspects each tools/call first Permission Protocol's MCP enforcement guide.

If you run the limiter inside an AI gateway rather than in your own code, check how the vendor evaluates rules. TrueFoundry documents that every matching limit is checked and all must allow the request, with usage counted against each, and that enforcement is eventually consistent so a burst of concurrent in-flight requests can overshoot slightly before blocking begins TrueFoundry's rate limiting docs. That overshoot note is a reason to keep write limits well below the capacity of the downstream.

Sizing the limit and combining limits without fighting yourself

Size the limit from the downstream, not from the agent's feelings. Solo.io's guidance is to start conservatively, monitor, and adjust against observed peak usage and infrastructure capacity Solo.io. For a write tool, add a second, coarser limit on the same caller: 20 writes per hour for the tool, plus 200 per day across all tools. Multiple limits combine as an AND, so the tighter one wins when it matters TrueFoundry.

Two anti-patterns to avoid. First, do not enforce only on the client; client side limits can be forged or bypassed, so the server side check is the one that counts ByteByteGo's rate limiter design. Second, do not set the read limit equal to the write limit just for tidiness. Reads can afford a bucket ten times the write cap; writes should never see a burst they cannot idempotently absorb.

For teams routing many models and tools through one place, the cap window decision is per tool, but the plumbing is shared. Quanta Inference (https://quantaintelligence.ai/inference) exposes text, image, video, audio and embedding models through one OpenAI compatible API key, which keeps the caller identity consistent across models so a per caller bucket means the same thing everywhere. And if you would rather have the cap, the tool policy and the audit trail handled by someone who does this for a living, a Quanta expert (https://quantaintelligence.ai/agents) can review your limits against your actual tool usage.

FAQ

FAQ

Which algorithm should I pick if I only get one?

Pick a token bucket and make the burst capacity small. It is constant size per caller, it tolerates the natural burstiness of an agent that thinks for a while and then acts, and it fails in a direction (clumped refusals) that a client can retry through GeeksforGeeks. Only move to a sliding log when the tool mutates state.

Why is a fixed window considered acceptable at all?

Because it costs two integers and is trivially explainable, and because the boundary double burst only matters when the action is expensive to repeat. Zuplo's comparison notes that fixed windows are simple to understand and debug but vulnerable to burst traffic at window boundaries Zuplo. Reserve it for coarse quotas, caches and read paths with idempotency.

My agent fans out hundreds of calls. Does a per caller bucket still help?

Yes, if the bucket is keyed to the tool and the caller, not just the account. NeuralTrust notes that a single prompt can trigger a cascade of calls, so a recursive loop is the realistic failure and a per tool cap stops it at the tool it is looping on NeuralTrust. Combine it with a coarse account limit as a backstop.

Do I need both rate limiting and throttling?

Usually yes. Rate limiting is the hard security and abuse control, while throttling is the softer fairness and latency control that keeps one client from monopolizing capacity NeuralTrust. Refuse with the limiter, and queue with the throttle so the limiter fires less often.

What should the refusal response contain?

A 429 with Retry-After computed from the algorithm's own state, plus a machine readable reason naming the limit that fired. TrueFoundry's docs note that matching limits are evaluated in alphabetical order by name, which affects only which limit is reported first on a blocked request TrueFoundry. Naming the fired limit saves your agents a guess.

Sources

Sources

  1. What Is LLM Function Calling?
  2. Rate Limiting & Throttling for AI Agents
  3. Rate Limiting Algorithms - System Design
  4. API Rate Limiting Platform Comparison: Zuplo vs Kong vs ...
  5. What Is Rate Limiting? Benefits, Techniques & Tips
  6. MCP Policy Enforcement: How to Secure Model Context ...
  7. Rate Limiting - TrueFoundry Docs
  8. Design A Rate Limiter
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles