Skip to content

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.

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.

Different endpoints have different resource costs and abuse risks. Flowstate uses 8 tiers to apply appropriate limits:

TierLimitWindowApplied To
ExemptUnlimitedN/AHealth checks, Agora tokens, SSE sync
Strict10 requests1 minuteMagic link requests, account deletion
Expensive5 requests1 minuteCV parsing (OpenAI), AI classification
Upload20 requests1 minuteFile uploads (avatars, attachments)
Canvas300 requests1 minutePosition updates, drag/drop operations
Write100 requests1 minuteCreate, update, delete operations
Read300 requests1 minuteList and fetch operations
Standard200 requests1 minuteDefault for uncategorized endpoints

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 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 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.

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 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 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 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.

The standard tier is the default for endpoints that don’t fit other categories. It allows 200 requests per minute, balancing usability and protection.

Rate limits are tracked differently for authenticated and unauthenticated requests:

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.

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.

The backend detects the real client IP address by checking proxy headers:

  1. CF-Connecting-IP (Cloudflare)
  2. X-Real-IP (nginx)
  3. X-Forwarded-For (AWS ALB, nginx, etc.)
  4. 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.

Rate limit state is stored in Redis for consistency across multiple backend instances. When a request arrives:

  1. Backend generates a rate limit key (e.g., ratelimit:write:user:123)
  2. Backend increments the key in Redis using INCR
  3. If the key didn’t exist, backend sets an expiry (60 seconds)
  4. Backend checks the current count
  5. If count exceeds the limit, backend returns 429 Too Many Requests
  6. 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.

Redis provides:

  • Atomic operations: INCR is 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.

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.

Every API response includes rate limit headers:

RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Reset: 1678901234

These 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.

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: 42

Clients should respect the Retry-After value and wait before retrying.

The standard tier limit is configurable via environment variables:

RATE_LIMIT_WINDOW_MS=60000 # 1 minute
RATE_LIMIT_MAX_REQUESTS=200 # 200 requests per minute

This 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).

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.

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.

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.

When designing new endpoints, follow these guidelines:

  • 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

Before deploying a new endpoint, test that the rate limit is appropriate:

  1. Simulate normal usage (how many requests would a typical user make?)
  2. Simulate heavy usage (how many requests would a power user make?)
  3. 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.

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 description

This helps developers understand the limits and design their applications accordingly.

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 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.

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)

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.