Rate Limiting
Flowstate uses an 8-tier rate limiting system to protect against abuse, denial-of-service attacks, and resource exhaustion. Rate limits are enforced on every API endpoint and backed by Redis for consistency across multiple backend instances.
Why Rate Limiting Matters
Section titled “Why Rate Limiting Matters”Without rate limiting, attackers can:
- Overwhelm the server: Send thousands of requests per second, exhausting CPU, memory, and network bandwidth
- Exhaust resources: Trigger expensive operations (like AI classification or file uploads) repeatedly
- Enumerate data: Rapidly test email addresses, usernames, or other identifiers to discover valid accounts
- Brute force: Try many passwords or magic link tokens in quick succession
- Spam: Flood the platform with unwanted content (messages, rooms, reports, etc.)
Rate limiting prevents these attacks by restricting how many requests a user or IP address can make within a time window.
8-Tier Rate Limiting System
Section titled “8-Tier Rate Limiting System”Different endpoints have different resource costs and abuse risks. Flowstate uses 8 tiers to apply appropriate limits:
| Tier | Limit | Window | Applied To |
|---|---|---|---|
| Exempt | Unlimited | N/A | Health checks, Agora tokens, SSE sync |
| Strict | 10 requests | 1 minute | Magic link requests, account deletion |
| Expensive | 5 requests | 1 minute | CV parsing (OpenAI), AI classification |
| Upload | 20 requests | 1 minute | File uploads (avatars, attachments) |
| Canvas | 300 requests | 1 minute | Position updates, drag/drop operations |
| Write | 100 requests | 1 minute | Create, update, delete operations |
| Read | 300 requests | 1 minute | List and fetch operations |
| Standard | 200 requests | 1 minute | Default for uncategorized endpoints |
Exempt Tier
Section titled “Exempt Tier”Some endpoints are exempt from rate limiting:
- Health checks (
/health): Monitoring systems need unrestricted access to verify service availability - Agora tokens (
/api/agora/token): Real-time communication requires frequent token refreshes - SSE sync (
/api/sync): Server-Sent Events connections are long-lived and don’t consume significant resources
Exempt endpoints are still authenticated (where applicable), but they don’t count against rate limits.
Strict Tier
Section titled “Strict Tier”Strict limits apply to sensitive operations that should be rare:
- Magic link requests (
POST /api/auth/magic-link): Sending emails is expensive and can be abused for spam - Account deletion (
POST /api/auth/delete-account): Deleting accounts is irreversible and should be deliberate
The strict tier allows only 10 requests per minute. This is enough for legitimate use (a user might request a magic link a few times if they don’t receive the email), but prevents abuse.
Expensive Tier
Section titled “Expensive Tier”Expensive operations consume significant resources:
- CV parsing (
POST /api/cv/parse): Calls OpenAI API, which has per-request costs - AI classification (
POST /api/classify): Uses machine learning models that require GPU time
The expensive tier allows only 5 requests per minute. This prevents users from exhausting API quotas or running up costs.
Upload Tier
Section titled “Upload Tier”File uploads consume bandwidth and storage:
- Avatar uploads (
POST /api/users/avatar) - Attachment uploads (
POST /api/attachments)
The upload tier allows 20 requests per minute. This is enough for normal use (uploading a profile picture or a few attachments), but prevents abuse (uploading thousands of files to exhaust storage).
Canvas Tier
Section titled “Canvas Tier”Canvas operations are high-frequency but low-cost:
- Position updates (
PATCH /api/canvas/blocks/:id/position) - Drag/drop operations (
POST /api/canvas/blocks/:id/move)
Users drag blocks around the canvas frequently, so the canvas tier allows 300 requests per minute (5 per second). This is enough for smooth interaction without allowing abuse.
Write Tier
Section titled “Write Tier”Write operations modify data:
- Create rooms (
POST /api/rooms) - Update profiles (
PATCH /api/users/me) - Delete messages (
DELETE /api/messages/:id)
The write tier allows 100 requests per minute. This is enough for normal use (creating a few rooms, updating a profile), but prevents spam (creating thousands of rooms).
Read Tier
Section titled “Read Tier”Read operations fetch data:
- List rooms (
GET /api/rooms) - Fetch user profile (
GET /api/users/:id) - Get messages (
GET /api/messages)
The read tier allows 300 requests per minute. Read operations are cheaper than writes (no database writes, no side effects), so the limit is higher.
Standard Tier
Section titled “Standard Tier”The standard tier is the default for endpoints that don’t fit other categories. It allows 200 requests per minute, balancing usability and protection.
Per-User vs Per-IP Tracking
Section titled “Per-User vs Per-IP Tracking”Rate limits are tracked differently for authenticated and unauthenticated requests:
Authenticated Requests (Per-User)
Section titled “Authenticated Requests (Per-User)”If a request includes a valid session token, the rate limit is tracked by user ID:
const key = `ratelimit:${tier}:user:${userId}`;This prevents shared-IP false positives. For example:
- Corporate networks: Many users share the same public IP address
- VPNs: All users on a VPN appear to come from the same IP
- Mobile networks: Carrier-grade NAT means many users share the same IP
Tracking by user ID ensures each user has their own rate limit, regardless of their IP address.
Unauthenticated Requests (Per-IP)
Section titled “Unauthenticated Requests (Per-IP)”If a request doesn’t include a session token (for example, magic link requests or public endpoints), the rate limit is tracked by IP address:
const key = `ratelimit:${tier}:ip:${ipAddress}`;This prevents attackers from bypassing rate limits by creating many accounts. Even if an attacker creates 100 accounts, they’re still limited by their IP address.
Real Client IP Detection
Section titled “Real Client IP Detection”The backend detects the real client IP address by checking proxy headers:
CF-Connecting-IP(Cloudflare)X-Real-IP(nginx)X-Forwarded-For(AWS ALB, nginx, etc.)req.ip(Express.js default)
The first header that exists is used as the client IP. This ensures rate limiting works correctly behind proxies and load balancers.
Redis-Backed Rate Limiting
Section titled “Redis-Backed Rate Limiting”Rate limit state is stored in Redis for consistency across multiple backend instances. When a request arrives:
- Backend generates a rate limit key (e.g.,
ratelimit:write:user:123) - Backend increments the key in Redis using
INCR - If the key didn’t exist, backend sets an expiry (60 seconds)
- Backend checks the current count
- If count exceeds the limit, backend returns 429 Too Many Requests
- If count is within the limit, backend processes the request
Redis automatically deletes keys after they expire, resetting the rate limit for the next window.
Why Redis?
Section titled “Why Redis?”Redis provides:
- Atomic operations:
INCRis atomic, so concurrent requests don’t corrupt the count - Shared state: All backend instances see the same rate limit state
- Automatic expiry: Keys are automatically deleted after the time window
- High performance: Redis can handle millions of operations per second
Without Redis, each backend instance would have its own rate limit state. A user could bypass limits by sending requests to different instances.
Graceful Degradation
Section titled “Graceful Degradation”If Redis is unavailable, rate limiting falls back to in-memory storage:
const inMemoryStore = new Map();
function rateLimit(key, limit) { try { // Try Redis first return await redisRateLimit(key, limit); } catch (error) { // Fall back to in-memory return inMemoryRateLimit(key, limit); }}In-memory rate limiting is per-instance (not shared), but it’s better than no rate limiting. The application remains functional even if Redis is down.
Rate Limit Headers
Section titled “Rate Limit Headers”Every API response includes rate limit headers:
RateLimit-Limit: 100RateLimit-Remaining: 87RateLimit-Reset: 1678901234These headers tell clients:
- RateLimit-Limit: Maximum requests allowed in the current window
- RateLimit-Remaining: How many requests are left in the current window
- RateLimit-Reset: Unix timestamp when the rate limit resets
Clients can use these headers to avoid hitting rate limits. For example, a client might pause requests when RateLimit-Remaining is low.
429 Too Many Requests Response
Section titled “429 Too Many Requests Response”When a rate limit is exceeded, the backend returns a 429 Too Many Requests response:
{ "error": "Rate limit exceeded", "message": "Too many requests. Please try again in 42 seconds.", "retryAfter": 42}The response includes:
- error: Error type (always “Rate limit exceeded”)
- message: Human-readable explanation with retry guidance
- retryAfter: Seconds until the rate limit resets
The Retry-After header is also included for compatibility with HTTP standards:
Retry-After: 42Clients should respect the Retry-After value and wait before retrying.
Configurable Standard Tier
Section titled “Configurable Standard Tier”The standard tier limit is configurable via environment variables:
RATE_LIMIT_WINDOW_MS=60000 # 1 minuteRATE_LIMIT_MAX_REQUESTS=200 # 200 requests per minuteThis allows operators to adjust rate limits based on traffic patterns and resource availability. Other tiers are hardcoded because they’re tied to specific resource costs (like OpenAI API quotas).
Rate Limiting by Endpoint
Section titled “Rate Limiting by Endpoint”Each endpoint is assigned a tier in the route definition:
// Strict tier (10 requests per minute)router.post('/api/auth/magic-link', rateLimit('strict'), async (req, res) => { // Send magic link email});
// Write tier (100 requests per minute)router.post('/api/rooms', rateLimit('write'), async (req, res) => { // Create room});
// Read tier (300 requests per minute)router.get('/api/rooms', rateLimit('read'), async (req, res) => { // List rooms});The rateLimit middleware checks the tier and enforces the limit before the route handler runs.
Rate Limiting for SSE Connections
Section titled “Rate Limiting for SSE Connections”Server-Sent Events (SSE) connections are long-lived HTTP connections that remain open for minutes or hours. Rate limiting SSE connections is tricky because:
- Connection establishment is rare: Users connect once and stay connected
- Events are server-initiated: The server sends events to the client, not the other way around
- Disconnections are common: Network issues, proxy timeouts, and server restarts cause frequent reconnections
SSE connections are exempt from rate limiting. However, the initial connection is authenticated, so only legitimate users can establish connections.
If a user repeatedly connects and disconnects (for example, to trigger server-side logic), the authentication rate limit (standard tier) applies. This prevents abuse while allowing normal reconnections.
Rate Limiting for Admin Endpoints
Section titled “Rate Limiting for Admin Endpoints”Admin endpoints use the same rate limiting tiers as user endpoints. However, admin actions are also logged in the audit trail, providing an additional layer of accountability.
If an admin exceeds a rate limit, they receive the same 429 response as a regular user. This prevents admins from accidentally or maliciously overwhelming the system.
Super admins are not exempt from rate limits. This ensures even the most privileged users can’t exhaust resources.
Rate Limiting Best Practices
Section titled “Rate Limiting Best Practices”When designing new endpoints, follow these guidelines:
Choose the Right Tier
Section titled “Choose the Right Tier”- Exempt: Only for health checks and real-time communication (SSE, WebSockets)
- Strict: Sensitive operations that should be rare (authentication, account deletion)
- Expensive: Operations that consume significant resources (AI, external APIs)
- Upload: File uploads and other bandwidth-intensive operations
- Canvas: High-frequency, low-cost operations (drag/drop, position updates)
- Write: Data modification operations (create, update, delete)
- Read: Data fetching operations (list, get)
- Standard: Default for uncategorized endpoints
Test Rate Limits
Section titled “Test Rate Limits”Before deploying a new endpoint, test that the rate limit is appropriate:
- Simulate normal usage (how many requests would a typical user make?)
- Simulate heavy usage (how many requests would a power user make?)
- Simulate abuse (how many requests would an attacker make?)
If normal usage exceeds the rate limit, increase the limit or move to a higher tier. If abuse is still possible, decrease the limit or move to a lower tier.
Document Rate Limits
Section titled “Document Rate Limits”API documentation should include rate limits for each endpoint:
## POST /api/rooms
Create a new room.
**Rate Limit**: 100 requests per minute (Write tier)
**Request Body**:
- `name` (string, required): Room name- `description` (string, optional): Room descriptionThis helps developers understand the limits and design their applications accordingly.
Rate Limiting Metrics
Section titled “Rate Limiting Metrics”The backend tracks rate limiting metrics:
- Total requests: How many requests were made
- Rate limited requests: How many requests were rejected due to rate limits
- Rate limit by tier: Breakdown of rate limited requests by tier
- Rate limit by endpoint: Which endpoints are most frequently rate limited
These metrics help identify:
- Abuse patterns: Sudden spikes in rate limited requests
- Misconfigured limits: Legitimate users hitting rate limits frequently
- Resource bottlenecks: Endpoints that need optimization or higher limits
Metrics are exported to monitoring systems (like Prometheus or Datadog) for alerting and analysis.
Rate Limiting and Denial-of-Service
Section titled “Rate Limiting and Denial-of-Service”Rate limiting is the first line of defense against denial-of-service (DoS) attacks. However, it’s not sufficient on its own:
- Distributed attacks: Attackers can use many IP addresses to bypass per-IP rate limits
- Application-layer attacks: Attackers can craft requests that consume disproportionate resources (like expensive database queries)
- Resource exhaustion: Even within rate limits, sustained traffic can exhaust resources
Additional protections include:
- Web Application Firewall (WAF): Blocks common attack patterns (SQL injection, XSS, etc.)
- DDoS protection: Cloudflare or AWS Shield absorbs large-scale attacks
- Auto-scaling: Backend instances scale up to handle increased traffic
- Circuit breakers: Expensive operations are disabled if they’re consuming too many resources
Rate limiting works best as part of a defense-in-depth strategy.
Rate Limiting Checklist
Section titled “Rate Limiting Checklist”Flowstate’s rate limiting system provides:
- ✅ 8-tier system (appropriate limits for different endpoint types)
- ✅ Per-user tracking (prevents shared-IP false positives)
- ✅ Per-IP tracking (prevents account creation bypass)
- ✅ Real client IP detection (works behind proxies and load balancers)
- ✅ Redis-backed (consistent across multiple backend instances)
- ✅ Graceful degradation (falls back to in-memory if Redis is unavailable)
- ✅ Standard headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset)
- ✅ 429 responses (clear error messages with retry guidance)
- ✅ Configurable standard tier (adjustable via environment variables)
- ✅ Metrics tracking (monitors rate limiting effectiveness)
Future Rate Limiting Enhancements
Section titled “Future Rate Limiting Enhancements”Planned improvements:
- Dynamic rate limits: Adjust limits based on user reputation or subscription tier
- Burst allowances: Allow short bursts above the limit (e.g., 10 requests in 1 second, but still 100 per minute)
- Endpoint-specific limits: Different limits for different endpoints within the same tier
- User-configurable limits: Allow users to set their own rate limits (for API integrations)
- Rate limit bypass tokens: Allow trusted clients to bypass rate limits
- Distributed rate limiting: Use a distributed counter (like Redis Cluster) for higher scale
- Machine learning: Detect and block suspicious traffic patterns automatically
Rate limiting is an ongoing focus. The platform evolves to address new attack patterns and incorporate industry best practices.