Skip to content

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_LIMITED event 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

retry after 5s
if still RATE_LIMITED → retry after 15s
→ retry after 30s (capped)

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 × limit per 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/websocket and src/interfaces/websocket.