Rate limiting
A rate limiter caps how fast each user can drive the agent, measured in model steps per second. Use one when many tenants share an agent and you need to smooth bursts or return HTTP 429s, rather than only cap total spend.
Budget caps bound total spend, but
do nothing about burst rate: a tenant can fire any number of steps
per second until the cumulative cap trips. TokenBucketRateLimiter
closes that gap. It is a classic token bucket, checked once per model
step from the shared budget gate, so every architecture is paced
through one place.
from loomflow import Agent, TokenBucketRateLimiter
agent = Agent(
"You are a support assistant.",
model="gpt-4.1-mini",
rate_limiter=TokenBucketRateLimiter(rps=5, burst=10),
)That’s the whole wiring. Every model step now acquires one token from
the calling user_id’s bucket before any provider call is made. The
default is None: no limiter, and the check is skipped entirely, so
an agent without a limiter pays zero overhead.
TokenBucketRateLimiter(...)
Every argument, with the keyword-only ones at their defaults:
from loomflow import TokenBucketRateLimiter
TokenBucketRateLimiter(
5.0, # rps: sustained refill rate (tokens/sec)
10, # burst: bucket capacity
per_user=True, # independent bucket per user_id
mode="throttle", # or "raise"
max_users=100_000,
user_idle_ttl_seconds=86_400,
)| Parameter | Description |
|---|---|
rps | Sustained rate: tokens accrue at rps per second, up to burst. Must be > 0. |
burst | Bucket capacity: how many steps a quiet tenant may fire back-to-back before pacing kicks in. Must be >= 1. |
per_user | True (default) keeps an independent bucket per user_id (the anonymous None id gets its own bucket). False shares one global bucket across all callers. |
mode | "throttle" (default) waits until a token accrues, smoothing bursts into the configured rate. "raise" raises RateLimitExceeded immediately when the bucket is empty. |
max_users / user_idle_ttl_seconds | Bounds on the per-user bucket map: the same LRU cap (100k tenants) and idle TTL (24h) that StandardBudget uses for its per-user accounting, so an adversarial stream of fresh user_id values can’t grow the map without limit. |
One tenant exhausting its bucket never affects another tenant’s; per-user isolation is the point.
Throttle vs raise
mode="throttle" queues: acquire sleeps until the refill rate
frees a token, so bursts are smoothed rather than failed. Use it when
the caller is a background job or an internal pipeline that should
simply slow down.
mode="raise" fails fast with a typed error carrying a retry
hint. Use it when you’d rather surface “slow down” (for example,
return a 429 to your own clients) than queue work:
from loomflow import Agent, TokenBucketRateLimiter
from loomflow.core.errors import RateLimitExceeded
agent = Agent(
"...", model="gpt-4.1-mini",
rate_limiter=TokenBucketRateLimiter(rps=2, burst=4, mode="raise"),
)
try:
result = await agent.run("hello", user_id="alice")
except RateLimitExceeded as exc:
print(exc.user_id) # "alice"
print(exc.retry_after) # e.g. 0.5: seconds until the next token
# → return HTTP 429 with a Retry-After headerRateLimitExceeded vs RateLimitError
Two deliberately distinct errors:
RateLimitExceeded | RateLimitError | |
|---|---|---|
| Who fired | Loomflow’s own admission gate, before any provider call | The provider returned a 429 / quota-exhausted response |
| When | The tenant’s token bucket is empty and mode="raise" | Mid-call, surfacing through a model adapter |
| Handled by | Your code (propagate a 429, back off, shed load) | The retry policy automatically (it subclasses TransientModelError) |
| Carries | user_id, retry_after (limiter’s estimate) | retry_after (when the provider supplied one) |
Catch RateLimitError for upstream quota problems; catch
RateLimitExceeded for the framework’s own gate.
Budgets vs rate limits
The two govern different axes; production deployments usually want both.
| Budget caps | Rate limiting | |
|---|---|---|
| Bounds | Cumulative spend (tokens, cost, wall-clock) | Instantaneous rate (steps per second) |
| Question answered | ”How much may this tenant consume, total?" | "How fast may this tenant consume it?” |
| Failure shape | Run halts; result.interrupted = True with a budget:* reason | Step waits (throttle) or RateLimitExceeded (raise) |
| Resets | Never (cumulative), or on your own schedule | Continuously, at rps tokens per second |
| Config | StandardBudget(BudgetConfig(...)) | TokenBucketRateLimiter(rps=, burst=) |
from datetime import timedelta
from loomflow import Agent, TokenBucketRateLimiter
from loomflow.governance.budget import BudgetConfig, StandardBudget
agent = Agent(
"...", model="gpt-4.1-mini",
budget=StandardBudget(BudgetConfig(per_user_max_cost_usd=2.0)),
rate_limiter=TokenBucketRateLimiter(rps=5, burst=10),
)Custom limiters: the RateLimiter protocol
The in-process token bucket is one implementation of a structural protocol. To plug in a distributed limiter (Redis, an API gateway), implement one method:
from loomflow import Agent
class RedisRateLimiter:
async def acquire(self, *, user_id: str | None) -> None:
# take one permit from your shared store;
# wait or raise RateLimitExceeded when none are available
...
agent = Agent("...", model="gpt-4.1-mini", rate_limiter=RedisRateLimiter())The agent loop calls acquire once before every model step, passing
the run’s user_id so per-tenant implementations can bucket
independently.
In-process state. TokenBucketRateLimiter buckets live in the
process. Behind a multi-worker deployment, each worker enforces the
limit independently, so N workers means an effective rate of N × rps
per tenant. For a fleet-wide limit, implement the RateLimiter
protocol against a shared store. Evicting an idle tenant’s bucket
(LRU / TTL) resets it to a full burst.