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.
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:There is also no configuration knob for it.
.env.exampleexposes: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
REQandCOUNTboth 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 exceededin agent logs (dozens of hits per agent-day) while the configured agent message limits are nowhere near exhausted. RaisingAGENT_STANDARD_MESSAGES_PER_MINhas 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: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:
and select on the already-resolved
is_agent(plus tier) when computingws_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_SECSalready 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.