Rate Limits¶
Rate limits protect the real-time API from floods and abusive clients. Limits are enforced per connection scope and per event type, using a sliding window per process.
Reference: errors.md · protocol.md
How limits are keyed¶
Each message is checked against two independent buckets per event type:
| Bucket key | Scope | Purpose |
|---|---|---|
user:<userId>:<eventType> | Per user | Prevent one user flooding a single event. |
ip:<ip>:<eventType> | Per IP | Prevent an IP flooding across accounts. |
The request is rejected if either bucket is exceeded. A cleared window resets the bucket.
Because buckets are process-local, each WebSocket server instance tracks its own limits. In a horizontally scaled deployment the effective global limit is
instances × per-instance limit. Use a shared (Redis) limiter if a strict global cap is required.
Window¶
The rate-limit window is 60 000 ms (WEBSOCKET_RATE_LIMIT_WINDOW_MS) by default. Per-event limits operate within this window.
Limit configuration¶
| Setting | Default | Description |
|---|---|---|
WEBSOCKET_DEFAULT_RATE_LIMIT | 120 | Messages per window for events without a specific limit. |
WEBSOCKET_RATE_LIMIT_WINDOW_MS | 60000 | Window length. |
| per-route override | — | Some routes set their own max; auction.bid.place uses 100. |
Per-event limits¶
| Event | Limit / window | Source |
|---|---|---|
auction.bid.place | 100 per minute | Route override |
chat.send (planned) | 30 per minute | Planned contract |
chat.typing (planned) | heavily throttled (e.g. 20 per 30s) | Planned contract |
auth.login (planned) | 10 per minute | Planned contract |
system.ping | 120 per minute | default |
auction.subscribe | 120 per minute | default |
auction.unsubscribe | 120 per minute | default |
| Staff commands | 120 per minute | default |
notification.read (planned) | 120 per minute | default |
| All other events | 120 per minute | default |
auth.login is listed because authentication commonly happens once per connection handshake; if it becomes an in-band event, 10/min is a safe limit.
Server behaviour when a limit is exceeded¶
When either bucket exceeds its limit for an event type, the server replies with RATE_LIMITED and does not execute the handler:
{
"type": "error",
"requestId": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"code": "RATE_LIMITED",
"message": "Too many WebSocket messages."
}
- The connection stays open; only the offending event is rejected.
- The client should stop sending that event and retry after the window resets (typically ≤ 60 s) or after an exponential backoff.
- Continuously hitting the limit is abusive; the platform may terminate connections or throttle users.
Client guidance¶
- Never auto-retry a
RATE_LIMITEDevent in a tight loop. - Throttle client-side typed sends (e.g. coalesce
chat.typing). - Back off interactively bids: if you have hit 100/min for
auction.bid.place, pause bidding until the window resets.
Example backoff¶
Scaling note¶
Because the default limiter is in-memory per process:
- Multiple connections from the same user on the same server share the user's bucket.
- Across servers, a user could send
instances × limitper window total. - If a single global cap is required (recommended for high-volume or anti-abuse goals), switch to a Redis-backed limiter keyed by the user/global event type. The interface is restricted because the backend teams own this trade-off; see
src/infrastructure/websocketandsrc/interfaces/websocket.
Related documents¶
- errors.md —
RATE_LIMITEDcode. - events/auction.md —
auction.bid.place(100/min). - events/chat.md — planned
chat.send/chat.typing. - protocol.md — middleware pipeline ordering (limits run before authorization).