Gateway Cluster-Wide Auth Rate Limiting

Share brute-force budgets across every gateway replica via a Redis-backed auth rate limiter

Set gateway.push.redis and every replica of your gateway shares one brute-force budget, so max_attempts on login and approval is a cluster-wide total — not a per-replica one.

Diagram

Cluster-Wide Auth Rate Limiting

under budget

over budget

🔑 Attempt
replica A

🔐 Shared Redis bucket
INCR + lockout

🔑 Attempt
replica B

✅ Allow

🚫 Lockout
cluster-wide

Quick Start

  1. Single replica — nothing to do

    One gateway process already enforces the limits correctly with the in-process AuthRateLimiter.

    python
    import asyncio
    from praisonaiagents import Agent
    from praisonai_bot.gateway import WebSocketGateway
    
    agent = Agent(name="Support", instructions="Help users")
    
    gateway = WebSocketGateway()            # in-process auth limits
    gateway.register_agent(agent)
    asyncio.run(gateway.start())
  2. Multiple replicas — set gateway.push.redis

    The auth limiters pick up the gateway’s push Redis automatically. There is no extra knob.

    yaml
    gateway:
      push:
        redis:
          url: redis://localhost:6379   # reused for auth rate limiting
  3. Or wire it in Python

    Pass a RedisConfig on GatewayConfig.push where you construct the gateway.

    python
    import asyncio
    from praisonaiagents import Agent
    from praisonaiagents.gateway import GatewayConfig, PushConfig, RedisConfig
    from praisonai_bot.gateway import WebSocketGateway
    
    agent = Agent(name="Support", instructions="Help users")
    
    gateway = WebSocketGateway(
        config=GatewayConfig(
            push=PushConfig(redis=RedisConfig(url="redis://localhost:6379")),
        ),
    )
    gateway.register_agent(agent)
    asyncio.run(gateway.start())

How It Works

Every attempt runs one atomic Lua script on Redis: check the lockout key, INCR the bucket, set its window TTL, and set a lockout once the budget is exceeded. All replicas hit the same keys, so counts never race.

Diagram
Redis(praison:push:authrl:magic_link:1.2.3.4)Replica BReplica ARedis(praison:push:authrl:magic_link:1.2.3.4)Replica BReplica AEVAL allow (attempts 1–3)1 — allowedEVAL allow (attempts 4–5)1 — allowedEVAL allow (attempt 6)0 — over budget, SET lockout PX 300000EVAL allow (attempt 7)0 — lockout key exists (cluster-wide)

Covered endpoints

LimiterBudgetWhat it protects
_approval_rate10 / 60 s window / 300 s lockoutApproval and pairing HTTP routes
_ws_upgrade_rate10 / 60 s window / 300 s lockoutWebSocket upgrade auth
_magic_rate5 / 60 s window / 300 s lockoutMagic-link login

Keys are namespaced as {prefix}authrl:{endpoint}:{identity}:b (bucket) and {prefix}authrl:{endpoint}:{identity}:l (lockout), where {prefix} is push.redis.prefix (default praison:push:). Both expire via Redis TTLs, so nothing needs pruning.


Redis Outage Behaviour

The limiter never fails open. If Redis is unreachable it falls back to a local per-replica limiter, so per-replica limits still apply.

Diagram

Yes

No

Redis call fails

after 5 s

⚙️ push.redis set

🔍 redis package
importable?

🔐 RedisAuthRateLimiter
cluster-wide

🗄️ In-process AuthRateLimiter
per-replica

⚠️ Local fallback
skip Redis for 5 s

SituationBehaviour
push.redis unsetIn-process AuthRateLimiter, exactly as before
redis package missing or client construction failsLogs gateway.push.redis set but Redis auth rate limiter unavailable (…); auth limits are per-process only. Install with: pip install redis and uses the in-process limiter
Redis call fails at runtimeLogs Redis auth rate limiter unavailable, using local: …, uses the local limiter, and skips Redis for 5 s so an outage can’t stall the event loop on every request
Lockout earned locally during an outageChecked before every call, so it outlives Redis recovery
Unreachable Redis hostClient uses socket_timeout=0.5 and socket_connect_timeout=0.5, so a call never blocks long

Configuration Options

The limiter reuses the gateway’s push RedisConfig — no separate auth-limiter Redis knob.

OptionWherePurpose
urlpush.redis.urlRedis connection URL, reused by the auth limiter
host / port / db / passwordpush.redis.*Field-wise connection when no url is given
prefixpush.redis.prefixNamespaces keys as `{prefix}authrl:{endpoint}:{identity}:{b\l} (default praison:push:`) so environments sharing one Redis do not collide

Best Practices

Enable it with the turn lock and idempotency

On a multi-replica gateway, the same push.redis block also backs the turn lock and the idempotency store. Configure all three together so every piece of shared state is cluster-wide.

Reuse the gateway's push RedisConfig

The auth limiter auto-shares push.redis. Configure Redis once; do not duplicate the connection.

Remember local lockouts outlive outages

A lockout earned against the local fallback stays in force after Redis comes back, so a locked-out IP stays locked out until its local lockout expires.

Grep for the unavailable log line

If you installed without the redis package, the gateway still boots but logs gateway.push.redis set but Redis auth rate limiter unavailable. Install it with pip install redis to get cluster-wide limits.


Related