Rate Limiting
This guide demonstrates rate limiting client messages to prevent abuse. The server can track rate limits per user, per document, or per user-document pair.
What it demonstrates
Section titled “What it demonstrates”- Configuring rate limiting on the server
- Tracking rate limits by user, document, or user-document pair
- Using persistent rate limit storage for multi-node deployments
- Handling rate limit exceeded events
Server Setup
Section titled “Server Setup”import { Server } from "teleportal/server";import { createStorage } from "unstorage";import { UnstorageDocumentStorage, UnstorageRateLimitStorage } from "teleportal/storage";
const storage = createStorage();
const server = new Server({ storage: new UnstorageDocumentStorage(storage), rateLimitConfig: { // Multiple rate limit rules rules: [ // Track by user (across all documents) { id: "per-user", maxMessages: 100, // 100 messages per window windowMs: 1000, // 1 second window trackBy: "user", }, // Track by document (across all users) { id: "per-document", maxMessages: 500, // 500 messages per window per document windowMs: 10000, // 10 second window trackBy: "document", }, // Track by user-document pair { id: "user-document", maxMessages: 100, windowMs: 1000, trackBy: "user-document", }, ], // Use persistent storage for multi-node deployments rateLimitStorage: new UnstorageRateLimitStorage(storage), // Callback when rate limit is exceeded onRateLimitExceeded: (details) => { console.warn("Rate limit exceeded", details); }, // Maximum message size maxMessageSize: 10 * 1024 * 1024, // 10MB // Callback when message size is exceeded onMessageSizeExceeded: (details) => { console.warn("Message size exceeded", details); }, },});Rate Limit Tracking Modes
Section titled “Rate Limit Tracking Modes”"user": Track rate limits per user ID. All connections from the same user share the same limit."document": Track rate limits per document ID. All users editing the same document share the same limit."user-document": Track rate limits per user-document pair. Each user has separate limits for each document."transport": Track rate limits per transport instance (in-memory only, not shared).
How the Token Bucket Works
Section titled “How the Token Bucket Works”Rate limiting uses a token bucket algorithm and is inbound-only: only messages arriving from clients are limited. Server-originated broadcasts are passed through untouched (silently dropping a doc update would permanently diverge a client until a full resync; egress is already bounded because every broadcast originates from a rate-limited ingress message).
Each tracking key (user, document, or user-document pair) gets a bucket that holds up to maxMessages tokens. Every incoming message consumes one token. Tokens refill at a steady rate of maxMessages per windowMs, so short bursts (up to maxMessages at once) are allowed while an average rate is enforced over time.
When a message arrives and the bucket is empty, it is held (delayed), not immediately dropped (default behavior). The message waits until its bucket refills, up to maxDelayMs (default 1000ms). Because each connection’s inbound stream is consumed sequentially, holding naturally throttles the sender losslessly. Only a wait that would exceed the budget causes the message to be dropped – and a dropped message fires onRateLimitExceeded and is nacked (an ACK carrying retryAfter) so the client can retransmit, never thrown. Set maxDelayMs: 0 for legacy drop-only behavior.
When multiple rules apply, all rules must pass, and a message rejected by a later rule refunds the tokens it already consumed from earlier rules, so a client’s retransmit isn’t double-charged.
ACK messages are automatically excluded from rate limiting since they are protocol-level acknowledgments, not user-initiated traffic. Rate limiting is also applied before permission checking – messages that exceed the limit are held or nacked without incurring the cost of a permission check.
Dynamic Rate Limits
Section titled “Dynamic Rate Limits”Both maxMessages and windowMs on each rule accept either a static number or a function that receives the current message and returns a number. This lets you vary limits based on user role, document type, or any other context.
import { Server } from "teleportal/server";
const server = new Server({ // ... rateLimitConfig: { rules: [ { id: "per-user", maxMessages: (msg) => { if (msg.context.role === "premium") return 500; return 100; }, windowMs: (msg) => { // Tighter window for public documents if (msg.document?.startsWith("public/")) return 500; return 1000; }, trackBy: "user", }, ], },});Skipping Rate Limits
Section titled “Skipping Rate Limits”Use shouldSkipRateLimit to bypass rate limiting entirely for certain messages. This is useful for admin users, internal service accounts, or specific message types that should never be throttled.
import { Server } from "teleportal/server";
const server = new Server({ // ... rateLimitConfig: { rules: [ { id: "per-user", maxMessages: 100, windowMs: 1000, trackBy: "user", }, ], shouldSkipRateLimit: async (msg) => { return msg.context.role === "admin"; }, },});Alerting on Rate Limit Exceeded
Section titled “Alerting on Rate Limit Exceeded”The onRateLimitExceeded callback fires every time a message is dropped due to rate limiting (after exhausting its allowed hold time). It receives structured details about the event, making it straightforward to integrate with logging and alerting systems. Note that resetAt is the timestamp when the next token refills – not the end of a full window – so a nacked sender only waits the fractional remainder of a single token before retrying.
import { Server } from "teleportal/server";
const server = new Server({ // ... rateLimitConfig: { rules: [ { id: "per-user", maxMessages: 100, windowMs: 1000, trackBy: "user", }, ], onRateLimitExceeded: (details) => { logger.warn("Rate limit exceeded", { ruleId: details.ruleId, userId: details.userId, documentId: details.documentId, currentCount: details.currentCount, maxMessages: details.maxMessages, resetAt: new Date(details.resetAt).toISOString(), }); // Send to your alerting service alertService.notify("rate-limit-exceeded", details); }, onMessageSizeExceeded: (details) => { logger.warn("Message size exceeded", { size: details.size, maxSize: details.maxSize, }); }, },});Multi-Node Deployments
Section titled “Multi-Node Deployments”For multi-node deployments, use persistent rate limit storage so that limits are shared across all server instances:
import { RedisRateLimitStorage } from "teleportal/transports/redis";
const rateLimitStorage = new RedisRateLimitStorage(redisClient);
const server = new Server({ // ... other options rateLimitConfig: { rateLimitStorage, // Shared across server instances // ... other options },});Without persistent storage, each server instance tracks limits independently (in-memory per transport), so a user could exceed the intended limit by connecting to multiple nodes.