Skip to content

WebSocket admission budget always uses the human per-second limit, even for agents; no agent WS tier exists #4440

Description

@CryptoJones

Summary

The WebSocket admission budget always uses the human per-second limit, even when the authenticated principal is an agent. The agent classification is resolved on the very next lines and applied to the Messages limit only.

crates/buzz-relay/src/connection.rs:

let (pubkey, is_agent) = { /* ... resolves agent_owner_pubkey ... */ };

let limits = &state.auth.config().rate_limits;
let (ws_window_secs, ws_limit) =
    crate::admission::ws_admission_budget(limits.human_ws_events_per_sec);  // <-- always human
// ... WsEvents check ...

if is_event {
    let message_limit = if is_agent {
        limits.agent_standard_messages_per_min      // <-- agent-aware
    } else {
        limits.human_messages_per_min
    };

There is also no configuration knob for it. .env.example exposes:

BUZZ_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC=10
BUZZ_RATE_LIMIT_AGENT_STANDARD_MESSAGES_PER_MIN=120
BUZZ_RATE_LIMIT_AGENT_STANDARD_API_CALLS_PER_MIN=600
BUZZ_RATE_LIMIT_AGENT_ELEVATED_MESSAGES_PER_MIN=300
BUZZ_RATE_LIMIT_AGENT_PLATFORM_MESSAGES_PER_MIN=600

Every other dimension has a human tier and one or more agent tiers. WS events has only the human tier, so elevated and platform agents get the same 10/sec ceiling as a person.

Why it matters

REQ and COUNT both pass through this check, so the budget governs subscription fan-out, not just writes. An agent that watches many channels spends its entire budget on subscriptions before doing any work.

On our deployment this shows up as rate-limited: quota exceeded in agent logs (dozens of hits per agent-day) while the configured agent message limits are nowhere near exhausted. Raising AGENT_STANDARD_MESSAGES_PER_MIN has no effect, because the rejection is coming from the WS dimension that has no agent tier.

The burst window is a fixed 5s window (WS_BURST_WINDOW_SECS), and the comment above it notes it was sized for desktop startup:

// Desktop startup establishes several independent live subscriptions at once.
// Preserve the configured average rate while allowing that bounded burst.

Clients that fan out more widely than desktop, and agents subscribed to many channels, both exceed it.

Suggested fix

Give WS events the same tiering as the other limits:

BUZZ_RATE_LIMIT_AGENT_STANDARD_WS_EVENTS_PER_SEC
BUZZ_RATE_LIMIT_AGENT_ELEVATED_WS_EVENTS_PER_SEC
BUZZ_RATE_LIMIT_AGENT_PLATFORM_WS_EVENTS_PER_SEC

and select on the already-resolved is_agent (plus tier) when computing ws_admission_budget.

If the current behaviour is intentional — one shared subscription ceiling regardless of principal — it would help to say so in .env.example, since the asymmetry with every other limit reads as an oversight.

Secondary note

The comment on WS_BURST_WINDOW_SECS already flags that a Redis-backed token bucket would behave better than a fixed window here. Fixed windows make bursts that straddle a boundary fail unpredictably, and a client that retries on rejection re-spends the same window, which turns one overflow into a sustained failure loop.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions