How WebSocket Message Rate Limiting (30 msg/10s) Prevents Abuse in TREK

The TREK server enforces a hard cap of 30 WebSocket messages per 10-second window per connection, automatically rejecting excess traffic with an error response to prevent spam, DoS attacks, and resource exhaustion.

The TREK repository implements per-connection throttling to protect its real-time collaboration features from abusive clients. By tracking message counts in a WeakMap tied to each socket, the server ensures that no single connection can dominate CPU, memory, or downstream resources like database updates and room broadcasts.

How the Rate Limiter Works in TREK

The throttling mechanism lives in server/src/websocket.ts, where the constants WS_MSG_LIMIT (30) and WS_MSG_WINDOW (10000ms) are defined at the top of the file【L27-L30】.

When a client transmits a message, the server retrieves a counter object from socketMsgCounts, a WeakMap that associates each WebSocket instance with its current rate-limiting state. The logic checks whether the current 10-second window has elapsed:

  • If the window expired: The counter resets to 1 with a new windowStart timestamp.
  • If still within the window: The count increments. If it exceeds 30, the server immediately sends {"type": "error", "message": "Rate limit exceeded"} and returns without processing the message further【L22-L30】.

This design ensures that the 31st message within any 10-second burst receives an instant error response, while legitimate traffic under the threshold passes through unaffected.

Threat Mitigation: What the Limit Blocks

The 30 msg/10s limit specifically targets four categories of abuse:

Spam and Flooding A malicious client cannot flood the server with thousands of rapid messages. After the 30th message, the connection receives an error and subsequent messages are ignored until the window resets, neutralizing volumetric spam attacks.

Denial-of-Service (DoS) By capping messages per socket, the server protects its event loop and memory from exhaustion. Even if an attacker attempts to overwhelm the server, the per-connection throttle prevents any single socket from consuming disproportionate CPU cycles.

Resource Exhaustion Downstream handlers—such as database writes, room broadcasts, and message persistence—are only triggered for the first 30 messages in each window. This prevents the database and broadcast subsystem from being buried under excessive write operations.

Fair Usage All connected users share the same 30-message quota. This ensures that one aggressive client cannot monopolize the channel bandwidth or server processing power at the expense of other collaborators.

Implementation Details

The rate-limiting logic is implemented directly in the message handler at lines 117-130 of server/src/websocket.ts:

// Rate limiting (lines 117-130)
const rate = socketMsgCounts.get(nws);
const now = Date.now();
if (now - rate.windowStart > WS_MSG_WINDOW) {
  rate.count = 1;
  rate.windowStart = now;
} else {
  rate.count++;
  if (rate.count > WS_MSG_LIMIT) {
    nws.send(JSON.stringify({ type: 'error', message: 'Rate limit exceeded' }));
    return;
  }
}

The socketMsgCounts WeakMap provides automatic cleanup when a WebSocket connection closes, preventing memory leaks from stale state. This per-connection approach (rather than per-user) means that even if an attacker opens many parallel browser tabs or connections, each socket is independently throttled.

Client-Side Examples

Normal usage (under the limit):

const ws = new WebSocket('wss://example.com/ws?token=…');
ws.onopen = () => {
  // Send up to 30 messages quickly – they are processed normally
  for (let i = 0; i < 25; i++) {
    ws.send(JSON.stringify({ type: 'ping', seq: i }));
  }
};
ws.onmessage = (e) => console.log('Server:', e.data);

Exceeding the limit:

const ws = new WebSocket('wss://example.com/ws?token=…');
ws.onopen = () => {
  // Send 35 messages rapidly – the 31st triggers the error response
  for (let i = 0; i < 35; i++) {
    ws.send(JSON.stringify({ type: 'spam', seq: i }));
  }
};
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === 'error' && msg.message === 'Rate limit exceeded') {
    console.warn('Server throttled us – back‑off needed');
  }
};

Testing the Rate Limit

The enforcement is validated in server/tests/websocket/connection.test.ts, which simulates a client sending 35 messages in rapid succession and asserts that the server emits the appropriate rate-limit error. This test ensures that the WeakMap tracking and window reset logic function correctly under load.

Summary

  • 30 messages per 10 seconds: The hard limit defined in server/src/websocket.ts prevents any single WebSocket from overwhelming the server.
  • WeakMap storage: Per-connection counters are stored in socketMsgCounts and automatically garbage-collected when sockets disconnect.
  • Instant error feedback: The 31st message triggers {"type": "error", "message": "Rate limit exceeded"} with no further processing.
  • Per-connection throttling: Each socket is rate-limited independently, preventing abuse even if an attacker opens multiple connections.
  • Protected downstream resources: Database updates and broadcasts are shielded from spam by blocking excess messages at the WebSocket layer.

Frequently Asked Questions

How does TREK handle rate limiting across multiple browser tabs from the same user?

Each browser tab creates a separate WebSocket connection, and TREK applies the 30 msg/10s limit per connection, not per user identity. This means every tab independently counts toward its own quota. Even if a user opens ten tabs, each one is throttled separately, preventing any single connection from abusing the system while ensuring fair distribution of server resources.

What happens to the 31st message sent within the 10-second window?

The server detects the threshold breach in the message handler at lines 117-130 of server/src/websocket.ts. It immediately transmits a JSON error payload {"type": "error", "message": "Rate limit exceeded"} to the client and returns without executing any downstream logic. The message is effectively dropped, protecting database and broadcast resources from unnecessary load.

Why does TREK use a WeakMap for tracking message counts instead of a regular Map?

The socketMsgCounts variable is a WeakMap that uses WebSocket objects as keys. This design allows the rate-limiting state to be automatically garbage-collected when a connection closes, preventing memory leaks. Unlike a regular Map, entries in a WeakMap do not prevent their keys from being garbage-collected, making the server more resilient during high-churn connection scenarios.

Can the rate limit be configured or disabled for specific deployment environments?

According to the source code in server/src/websocket.ts, the limits are defined as constants at lines 27-30 (WS_MSG_LIMIT and WS_MSG_WINDOW). These values appear to be fixed at the module level. To modify the threshold, an administrator would need to adjust these constants in the source code and redeploy the server, as there is no evidence of runtime environment variable configuration for these specific limits in the analyzed implementation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →