Gateway Rate Limit

Bound inbound turns per identity/scope with a config-driven sliding window, or inject a custom limiter

python
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")
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()

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.

Diagram

Gateway Rate Limit

allowed=True

allowed=False

📥 Inbound Turn

⚙️ RateLimitPolicy.check\nidentity · scope · now

✅ Allow Turn

🛑 Deny + retry_after

Quick Start

  1. 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: 20

    The 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
    AgentAdmissionGateBotUserAgentAdmissionGateBotUser37 seconds later…21st message within 60sadmit(session_id=”alice”)AdmissionRejected(retry_after=37)“Rate limit reached — please try again in 37s.”Next messageadmit(…)ADMITRun turnReply
    ValueBehaviour
    0 (default)Unlimited — no-op
    2020 turns per minute per identity
    -1ValueError at startup
    0.5 or trueValueError at startup

Choosing which path

Diagram

Just a per-minute cap

Need a cooldown lockout

Redis / per-tenant / cost-based

Need per-user budget?

gateway.per_user_per_minute: N

SlidingWindowRateLimitPolicy(…, lockout_seconds=N)

Custom RateLimitPolicyProtocol

For a cooldown penalty, Redis-backed state, or per-tenant limits, use the policy surface:

  1. 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()
  2. 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()
  3. YAML configuration

    Configure via gateway.yaml. For a plain per-minute cap, gateway.per_user_per_minute: N is all you need; use the rate_limit block 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
  4. Custom limiter

    Implement RateLimitPolicyProtocol for 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

Diagram
AgentRateLimitPolicyGatewayUserAgentRateLimitPolicyGatewayUseralt[allowed=True][allowed=False]Inbound turncheck(identity=user_id, scope=endpoint, now=time.time())RateLimitDecision(allowed=True)Run turnReplyReplyRateLimitDecision(allowed=False, retry_after_seconds=N)ConnectErrorCode.RATE_LIMITED + HelloError.retry_after_seconds=N
ComponentRole
identityThe caller — authenticated tenant, user ID, or endpoint principal
scopeEndpoint class, channel name, or tenant token
nowCurrent timestamp (seconds) — injected so decisions are pure and testable
RateLimitDecisionClosed result: allowed bool + optional retry_after_seconds backoff hint

Configuration Options

SlidingWindowRateLimitPolicy accepts three knobs:

OptionTypeDefaultDescription
max_requestsint0Maximum requests allowed per window. 0 disables the gate (legacy pass-through).
window_secondsfloat60.0Window duration in seconds. Must be > 0 or ValueError is raised.
lockout_secondsfloat0.0Penalty lockout after exceeding the ceiling. 0 means no lockout. Must be >= 0 or ValueError is raised.
python
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 > 0

Decision Table

Per (scope, identity) key:

Situationallowedretry_after_seconds
max_requests == 0 (disabled)TrueNone
Active lockout (now < lockout_until)Falselockout_until - now
New or expired windowTrueNone
Below ceiling in current windowTrueNone
Over ceiling, lockout_seconds > 0Falselockout_seconds (window dropped; key locked)
Over ceiling, lockout_seconds == 0Falsewindow_seconds - (now - window_start) (window preserved — not reset)

Choosing Lockout vs. No Lockout

Diagram

Yes

No

Want a cooldown penalty\nafter abuse?

Set lockout_seconds > 0\n(e.g. lockout_seconds=300)\nIdentity locked for N seconds\nafter hitting the ceiling

Leave lockout_seconds=0\nStrict per-window quota\nDeny until window rolls over\nno extra penalty


Stable identity

gateway.per_user_per_minute charges each turn to a caller identity the client cannot reset.

Entry pointRate key
Channel bots (Telegram, Discord, Slack, WhatsApp, …) and other admission callerssession_id (the per-user storage key)
HTTP APItoken:<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 turnsip:<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.

python
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 raise TypeError.
  • Return a closed RateLimitDecision — never None.
  • @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.


Related