Gateway Rate Limit
Bound inbound turns per identity/scope with a config-driven sliding window, or inject a custom limiter
from praisonaiagents import Agent
agent = Agent(name="assistant", instructions="Be helpful.")
# Per-client gateway rate limits protect this agent from traffic spikes
agent.start("Quick question")from praisonai_bot.bots import BotOS
from praisonaiagents import Agent
agent = Agent(name="Support", instructions="Help users")
bot = BotOS(agent=agent, platforms=["telegram"], per_user_per_minute=20)
bot.run()One line protects every connected channel — Telegram, Discord, Slack, HTTP, WebSocket.
Gateway rate limiting completes the gateway’s policy-protocol family: bound how often an identity can start turns in a given scope with the built-in sliding window, or inject your own limiter.
The user hits the gateway repeatedly; per-client rate limits cap requests and return clear backoff signals.
Quick Start
- Simplest: one YAML key
Cap how many turns one user may start per minute — unlimited by default (
0).yaml # gateway.yaml (or botos.yaml under the same gateway: block) gateway: per_user_per_minute: 20The equivalent Python one-liner:
python from praisonai_bot.bots import BotOS from praisonaiagents import Agent agent = Agent(name="Support", instructions="Help users") bot = BotOS(agent=agent, platforms=["telegram"], per_user_per_minute=20) bot.run()Over the budget, the user sees: “Rate limit reached — please try again in Ns.” The turn is refused before any agent work starts. A fresh conversation does not reset the budget — the gateway charges a stable identity (session id on bot channels, authenticated token or peer IP on HTTP / WebSocket).
Diagram Value Behaviour 0(default)Unlimited — no-op 2020 turns per minute per identity -1ValueErrorat startup0.5ortrueValueErrorat startup
Choosing which path
For a cooldown penalty, Redis-backed state, or per-tenant limits, use the policy surface:
- Simple sliding window
Five turns per minute per identity.
max_requests=0(default) disables the gate entirely — safe for dev environments.python from praisonaiagents import Agent from praisonaiagents.gateway import SlidingWindowRateLimitPolicy from praisonai.bots import BotOS agent = Agent(name="Support", instructions="Help users") rate_limit = SlidingWindowRateLimitPolicy( max_requests=5, window_seconds=60, ) bot = BotOS( agent=agent, platforms=["telegram"], rate_limit_policy=rate_limit, ) bot.start() - With lockout cooldown
After hitting the ceiling, lock out the identity for an additional cooldown period.
python from praisonaiagents import Agent from praisonaiagents.gateway import SlidingWindowRateLimitPolicy from praisonai.bots import BotOS agent = Agent(name="Support", instructions="Help users") rate_limit = SlidingWindowRateLimitPolicy( max_requests=5, window_seconds=60, lockout_seconds=300, # 5-minute penalty after abuse ) bot = BotOS( agent=agent, platforms=["telegram"], rate_limit_policy=rate_limit, ) bot.start() - YAML configuration
Configure via
gateway.yaml. For a plain per-minute cap,gateway.per_user_per_minute: Nis all you need; use therate_limitblock when you need a custom window or a lockout:yaml gateway: rate_limit: max_requests: 5 window_seconds: 60 lockout_seconds: 0 # 0 = no lockout, deny until window rolls over - Custom limiter
Implement
RateLimitPolicyProtocolfor Redis-backed, per-tenant, or cost-based limits:python import redis from praisonaiagents.gateway import RateLimitDecision, RateLimitPolicyProtocol from praisonai.bots import BotOS from praisonaiagents import Agent redis_client = redis.Redis(host="localhost", port=6379, db=0) class RedisRateLimitPolicy: def check(self, *, identity: str, scope: str, now: float) -> RateLimitDecision: key = f"rl:{scope}:{identity}" count = redis_client.incr(key) if count == 1: redis_client.expire(key, 60) if count > 10: return RateLimitDecision(allowed=False, retry_after_seconds=60.0) return RateLimitDecision(allowed=True) assert isinstance(RedisRateLimitPolicy(), RateLimitPolicyProtocol) agent = Agent(name="Support", instructions="Help users") bot = BotOS(agent=agent, platforms=["telegram"], rate_limit_policy=RedisRateLimitPolicy()) bot.start()
How It Works
| Component | Role |
|---|---|
identity | The caller — authenticated tenant, user ID, or endpoint principal |
scope | Endpoint class, channel name, or tenant token |
now | Current timestamp (seconds) — injected so decisions are pure and testable |
RateLimitDecision | Closed result: allowed bool + optional retry_after_seconds backoff hint |
Configuration Options
SlidingWindowRateLimitPolicy accepts three knobs:
| Option | Type | Default | Description |
|---|---|---|---|
max_requests | int | 0 | Maximum requests allowed per window. 0 disables the gate (legacy pass-through). |
window_seconds | float | 60.0 | Window duration in seconds. Must be > 0 or ValueError is raised. |
lockout_seconds | float | 0.0 | Penalty lockout after exceeding the ceiling. 0 means no lockout. Must be >= 0 or ValueError is raised. |
from praisonaiagents.gateway import SlidingWindowRateLimitPolicy
policy = SlidingWindowRateLimitPolicy(
max_requests=10,
window_seconds=60.0,
lockout_seconds=0.0,
)
print(policy.enabled) # True when max_requests > 0Decision Table
Per (scope, identity) key:
| Situation | allowed | retry_after_seconds |
|---|---|---|
max_requests == 0 (disabled) | True | None |
Active lockout (now < lockout_until) | False | lockout_until - now |
| New or expired window | True | None |
| Below ceiling in current window | True | None |
Over ceiling, lockout_seconds > 0 | False | lockout_seconds (window dropped; key locked) |
Over ceiling, lockout_seconds == 0 | False | window_seconds - (now - window_start) (window preserved — not reset) |
Choosing Lockout vs. No Lockout
Stable identity
gateway.per_user_per_minute charges each turn to a caller identity the client cannot reset.
| Entry point | Rate key |
|---|---|
| Channel bots (Telegram, Discord, Slack, WhatsApp, …) and other admission callers | session_id (the per-user storage key) |
| HTTP API | token:<sha256-prefix> only when the bearer equals the gateway’s configured auth_token (constant-time compared); otherwise ip:<peer-host>; ip:unknown when the peer is absent |
| WebSocket turns | ip:<peer-host> from the live connection; ip:unknown when absent |
Rotating a bearer token, cycling x-session-id, or opening a new WebSocket conversation does not mint a fresh budget — the caller keeps hitting the same token- or IP-keyed budget.
Building a Custom Limiter
Any object with a check(*, identity, scope, now) method satisfies RateLimitPolicyProtocol. The signature must be keyword-only.
from praisonaiagents.gateway import RateLimitDecision, RateLimitPolicyProtocol
class TenantRateLimitPolicy:
def __init__(self, limits: dict[str, int]):
self._limits = limits
self._counts: dict[tuple, int] = {}
def check(self, *, identity: str, scope: str, now: float) -> RateLimitDecision:
limit = self._limits.get(identity, 5)
key = (scope, identity)
count = self._counts.get(key, 0) + 1
self._counts[key] = count
if count > limit:
return RateLimitDecision(allowed=False, retry_after_seconds=60.0)
return RateLimitDecision(allowed=True)
policy = TenantRateLimitPolicy(limits={"premium-tenant": 100, "free-tenant": 10})
assert isinstance(policy, RateLimitPolicyProtocol)Key requirements:
- Keyword-only args —
check(*, identity, scope, now)— positional calls will raiseTypeError. - Return a closed
RateLimitDecision— neverNone. @runtime_checkable—isinstance(policy, RateLimitPolicyProtocol)works at runtime.
State Ownership
Best Practices
Disable in dev, enable deliberately in production
max_requests=0 (the default) disables rate limiting entirely. This is the safe default for development. Turn on the gate explicitly when you deploy to production — don’t rely on it being on by default.
Prefer window-only limiting unless you want an abuse penalty
lockout_seconds=0 (the default) gives you a strict per-window quota with no extra penalty. The identity is simply denied until the window rolls over. Set lockout_seconds only if you want a cooldown penalty after a burst — e.g. lockout_seconds=300 for a 5-minute cool-down after hitting the ceiling.
Implement the protocol for per-tenant or distributed limits
SlidingWindowRateLimitPolicy uses in-process state. For per-tenant pricing, multi-process gateways, or Redis-backed distributed limits, implement RateLimitPolicyProtocol yourself — the seam is intentional. The check method is the only surface you need to implement.
Keep identity and scope cardinality bounded
Each (scope, identity) pair gets its own state entry. If your identity space is unbounded (e.g. raw IPs), entries accumulate without eviction. Either use the gateway’s built-in limiters (which handle reclamation) or implement your own with periodic cleanup.