Skip to content

Rate Limiting

Seed implements distributed rate limiting using Redis-backed sliding window algorithm to protect against abuse and ensure fair resource usage across multiple endpoints.

Overview

The rate limiting system provides:

  • Distributed Rate Limiting: Redis-backed storage works across multiple server instances
  • Sliding Window Algorithm: More accurate than fixed windows, prevents burst traffic at window boundaries
  • Per-IP Limiting: Prevents individual IPs from overwhelming the service
  • Global Limiting: Protects against distributed attacks across many IPs
  • Graceful Degradation: Falls back to allowing requests if Redis is unavailable
  • Configurable Windows: Different limits for different endpoint types

Architecture

Components

Redis Storage

Rate limiting uses Redis sorted sets for efficient sliding window implementation:

Key: ratelimit:mcp:192.168.1.100
Value: Sorted Set
  - Member: request_timestamp_1
  - Score: timestamp_1
  - Member: request_timestamp_2
  - Score: timestamp_2
  ...

TTL Management:

  • Keys expire after the window duration
  • Old entries are removed before counting
  • Atomic operations ensure accuracy

Implementation

File: src/middleware/distributed-rate-limit.ts

The rate limiter is implemented as Express middleware using the factory pattern:

typescript
interface RateLimitConfig {
  windowMs: number;           // Time window in milliseconds
  maxRequests: number;        // Max requests per IP per window
  globalMax: number;          // Max requests globally per window
  keyPrefix: string;          // Redis key prefix
  endpointType: string;       // For logging/metrics (e.g., "mcp", "dcr")
}

function createDistributedRateLimiter(config: RateLimitConfig) {
  return async (req: Request, res: Response, next: NextFunction) => {
    // Implementation
  };
}

Sliding Window Algorithm

Step 1: Remove Old Entries

typescript
const now = Date.now();
const windowStart = now - config.windowMs;

// Remove entries outside the current window
await redis.zremrangebyscore(key, '-inf', windowStart);

Step 2: Count Current Requests

typescript
// Count requests in current window
const count = await redis.zcard(key);

Step 3: Check Limits

typescript
// Per-IP limit check
if (count >= config.maxRequests) {
  return sendRateLimitError(res, config.windowMs);
}

// Global limit check
const globalKey = `${config.keyPrefix}global`;
const globalCount = await redis.zcard(globalKey);
if (globalCount >= config.globalMax) {
  return sendRateLimitError(res, config.windowMs);
}

Step 4: Add Current Request

typescript
// Add current request to sorted set
await redis.zadd(key, now, `${now}-${Math.random()}`);

// Set expiry on key
await redis.expire(key, Math.ceil(config.windowMs / 1000));

IP Address Extraction

typescript
function getClientIp(req: Request): string {
  // Check X-Forwarded-For header (for proxied requests)
  const forwarded = req.headers['x-forwarded-for'];
  if (forwarded) {
    return Array.isArray(forwarded)
      ? forwarded[0].split(',')[0].trim()
      : forwarded.split(',')[0].trim();
  }

  // Check X-Real-IP header
  const realIp = req.headers['x-real-ip'];
  if (realIp) {
    return Array.isArray(realIp) ? realIp[0] : realIp;
  }

  // Fallback to socket IP
  return req.socket.remoteAddress || 'unknown';
}

Endpoint Configuration

MCP Endpoint Rate Limiting

Applied to: POST /mcp, DELETE /mcp

Configuration:

bash
MCP_RATE_LIMIT_WINDOW_MS=60000       # 1 minute
MCP_RATE_LIMIT_MAX=100               # 100 requests per IP per minute
MCP_GLOBAL_RATE_LIMIT_MAX=10000      # 10,000 requests globally per minute

Rationale:

  • MCP requests are high-value protocol operations
  • 100 req/min per IP allows ~1.6 req/sec for normal usage
  • Global limit protects against distributed attacks
  • 1-minute window balances protection and usability

Usage:

typescript
// src/routes/mcp.ts
import { createDistributedRateLimiter } from '../middleware/distributed-rate-limit.js';
import { config } from '../config/index.js';

const mcpRateLimiter = createDistributedRateLimiter({
  windowMs: config.rateLimit.mcp.windowMs,
  maxRequests: config.rateLimit.mcp.maxRequests,
  globalMax: config.rateLimit.mcp.globalMax,
  keyPrefix: 'ratelimit:mcp:',
  endpointType: 'mcp',
});

router.post('/mcp', mcpRateLimiter, handleMcpRequest);
router.delete('/mcp', mcpRateLimiter, handleMcpDelete);

DCR Endpoint Rate Limiting

Applied to: POST /oauth/register

Configuration:

bash
DCR_RATE_LIMIT_WINDOW_MS=3600000     # 1 hour
DCR_RATE_LIMIT_MAX=10                # 10 registrations per IP per hour
DCR_GLOBAL_RATE_LIMIT_MAX=1000       # 1,000 registrations globally per hour

Rationale:

  • Client registration is infrequent (once per client)
  • Strict limits prevent registration abuse
  • 10 reg/hour per IP is generous for legitimate use
  • Global limit protects against distributed registration attacks

Usage:

typescript
// src/routes/oauth-register.ts
const dcrRateLimiter = createDistributedRateLimiter({
  windowMs: config.rateLimit.dcr.windowMs,
  maxRequests: config.rateLimit.dcr.maxRequests,
  globalMax: config.rateLimit.dcr.globalMax,
  keyPrefix: 'ratelimit:dcr:',
  endpointType: 'dcr',
});

router.post('/oauth/register', dcrRateLimiter, handleRegistration);

OAuth Authorize Endpoint Rate Limiting

Applied to: GET /oauth/authorize

Configuration:

bash
OAUTH_AUTHORIZE_RATE_LIMIT_WINDOW_MS=60000   # 1 minute
OAUTH_AUTHORIZE_RATE_LIMIT_MAX=10            # 10 authorizations per IP per minute
OAUTH_AUTHORIZE_GLOBAL_RATE_LIMIT_MAX=1000   # 1,000 authorizations globally per minute

Rationale:

  • OAuth authorization requests are user-facing and initiated during login flows
  • 10 req/min per IP prevents automated authorization request spam
  • Protects against DoS attacks via redirect loops
  • Global limit protects against distributed authorization flooding
  • 1-minute window provides quick recovery for legitimate retries

Usage:

typescript
// src/routes/oauth-authorize.ts
const authorizeRateLimiter = createDistributedRateLimiter({
  windowMs: config.rateLimit.authorize.windowMs,
  maxRequests: config.rateLimit.authorize.maxRequests,
  globalMax: config.rateLimit.authorize.globalMax,
  keyPrefix: 'ratelimit:authorize:',
  endpointType: 'authorize',
});

router.get('/oauth/authorize', authorizeRateLimiter, handleAuthorization);

Response Format

Success Response

When rate limit is not exceeded, the middleware adds headers to the response:

http
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1704643200

Headers:

  • X-RateLimit-Limit: Maximum requests allowed in window
  • X-RateLimit-Remaining: Requests remaining in current window
  • X-RateLimit-Reset: Unix timestamp when the limit resets

Rate Limit Exceeded

http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704643200
Retry-After: 45
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "error": {
    "code": -32000,
    "message": "Too Many Requests",
    "data": {
      "reason": "rate_limit_exceeded",
      "details": "Rate limit exceeded. Please try again later.",
      "retryAfter": 45
    }
  },
  "id": null
}

Additional Headers:

  • Retry-After: Seconds until the client can retry

Global Toggle

Rate limiting can be disabled globally via configuration:

bash
RATE_LIMIT_ENABLED=false

When disabled:

  • All rate limit middleware is bypassed
  • No Redis operations performed
  • All requests allowed through
  • Useful for development/testing

Implementation:

typescript
export function createDistributedRateLimiter(config: RateLimitConfig) {
  return async (req: Request, res: Response, next: NextFunction) => {
    // Check global toggle
    if (!rateLimitConfig.enabled) {
      return next();
    }

    // ... rate limiting logic
  };
}

Graceful Degradation

If Redis is unavailable, the rate limiter gracefully degrades:

typescript
try {
  // Rate limiting logic using Redis
} catch (error) {
  logger.error('Rate limiting Redis error', {
    error: error.message,
    endpoint: config.endpointType,
    ip: clientIp,
  });

  // Allow request through (fail open)
  return next();
}

Behavior:

  • Errors logged but don't block requests
  • Prevents rate limiting from causing downtime
  • Monitors should alert on Redis errors
  • Consider fail-closed behavior for critical endpoints

Observability

Prometheus Metrics

Request Counters:

typescript
const rateLimitRequests = new promClient.Counter({
  name: 'http_request_rate_limit_requests_total',
  help: 'Total HTTP requests evaluated by rate limiter',
  labelNames: ['endpoint', 'limited'],
});

// Usage
rateLimitRequests.inc({ endpoint: 'mcp', limited: 'false' });
rateLimitRequests.inc({ endpoint: 'dcr', limited: 'true' });

Example Queries:

promql
# Rate limit success rate
rate(http_request_rate_limit_requests_total{limited="false"}[5m])
/ rate(http_request_rate_limit_requests_total[5m])

# Rejection rate by endpoint
rate(http_request_rate_limit_requests_total{limited="true"}[5m])

# MCP endpoint load
sum(rate(http_request_rate_limit_requests_total{endpoint="mcp"}[5m]))

# DCR abuse detection
rate(http_request_rate_limit_requests_total{endpoint="dcr",limited="true"}[5m]) > 0.1

Structured Logging

Request Allowed:

typescript
logger.debug('Rate limit check passed', {
  endpoint: config.endpointType,
  ip: clientIp,
  count: currentCount,
  limit: config.maxRequests,
});

Request Blocked:

typescript
logger.warn('Rate limit exceeded', {
  endpoint: config.endpointType,
  ip: clientIp,
  count: currentCount,
  limit: config.maxRequests,
  retryAfter: Math.ceil(config.windowMs / 1000),
});

Redis Errors:

typescript
logger.error('Rate limiting Redis error', {
  error: error.message,
  endpoint: config.endpointType,
  ip: clientIp,
  action: 'failing_open',
});

Configuration Reference

Environment Variables

bash
# Global toggle
RATE_LIMIT_ENABLED=true              # Enable/disable rate limiting

# MCP endpoint
MCP_RATE_LIMIT_WINDOW_MS=60000       # 1 minute window
MCP_RATE_LIMIT_MAX=100               # Per-IP limit
MCP_GLOBAL_RATE_LIMIT_MAX=10000      # Global limit

# DCR endpoint
DCR_RATE_LIMIT_WINDOW_MS=3600000     # 1 hour window
DCR_RATE_LIMIT_MAX=10                # Per-IP limit
DCR_GLOBAL_RATE_LIMIT_MAX=1000       # Global limit

# Redis (required for rate limiting)
REDIS_URL=redis://redis:6379

Code Configuration

typescript
// src/config/rate-limit.ts
export const rateLimitConfig = {
  enabled: process.env.RATE_LIMIT_ENABLED !== "false",

  mcp: {
    windowMs: parseInt(process.env.MCP_RATE_LIMIT_WINDOW_MS ?? "60000", 10),
    maxRequests: parseInt(process.env.MCP_RATE_LIMIT_MAX ?? "100", 10),
    globalMax: parseInt(process.env.MCP_GLOBAL_RATE_LIMIT_MAX ?? "10000", 10),
  },

  dcr: {
    windowMs: parseInt(process.env.DCR_RATE_LIMIT_WINDOW_MS ?? "3600000", 10),
    maxRequests: parseInt(process.env.DCR_RATE_LIMIT_MAX ?? "10", 10),
    globalMax: parseInt(process.env.DCR_GLOBAL_RATE_LIMIT_MAX ?? "1000", 10),
  },
};

Testing

Unit Testing

typescript
describe('Rate Limiting', () => {
  it('should allow requests under limit', async () => {
    const req = createMockRequest({ ip: '192.168.1.100' });
    const res = createMockResponse();
    const next = vi.fn();

    await rateLimiter(req, res, next);

    expect(next).toHaveBeenCalled();
    expect(res.status).not.toHaveBeenCalled();
  });

  it('should block requests over limit', async () => {
    // Make maxRequests + 1 requests
    for (let i = 0; i <= maxRequests; i++) {
      await rateLimiter(req, res, next);
    }

    expect(res.status).toHaveBeenCalledWith(429);
  });

  it('should reset after window expires', async () => {
    // Make maxRequests requests
    // Wait for window to expire
    // Make another request - should succeed
  });
});

Best Practices

Choosing Limits

Considerations:

  1. Normal Usage Patterns: What's typical request frequency?
  2. Burst Allowance: Should short bursts be allowed?
  3. Attack Mitigation: What prevents abuse?
  4. User Experience: Does limit impact legitimate users?

Guidelines:

  • Start conservative, increase if needed
  • Monitor rejection rates
  • Different limits for different endpoints
  • Consider authenticated vs anonymous differently

Window Duration

Short Windows (1 minute):

  • ✅ Quick recovery from limits
  • ✅ Better for high-frequency operations
  • ❌ Less effective against slow attacks

Long Windows (1 hour):

  • ✅ Effective against sustained attacks
  • ✅ Good for infrequent operations
  • ❌ Long cooldown period

Global vs Per-IP

Per-IP Limits:

  • Protect against single source attacks
  • Fair resource distribution
  • Can be bypassed with IP rotation

Global Limits:

  • Protect against distributed attacks
  • Prevent service degradation
  • May impact all users during attack

Best Practice: Use both per-IP and global limits

Troubleshooting

High Rejection Rate

Symptoms:

  • Many 429 responses
  • Users reporting access issues

Diagnosis:

promql
# Check rejection rate
rate(http_request_rate_limit_requests_total{limited="true"}[5m])

# Check per-IP distribution
topk(10, sum by (ip) (rate(http_request_rate_limit_requests_total[5m])))

Solutions:

  • Increase rate limits if legitimate traffic
  • Investigate IPs with high request rates
  • Add IP to blocklist if malicious

Redis Connection Issues

Symptoms:

  • Rate limiting not working
  • All requests allowed through
  • Redis error logs

Diagnosis:

bash
# Check Redis connectivity
redis-cli -h redis ping

# Check Seed logs
grep "Rate limiting Redis error" logs.json

Solutions:

  • Verify Redis is running
  • Check REDIS_URL configuration
  • Review Redis resource limits
  • Consider fail-closed behavior

Uneven Distribution

Symptoms:

  • Some users blocked, others not
  • Inconsistent rate limit behavior

Diagnosis:

  • Check IP extraction logic
  • Verify proxy headers (X-Forwarded-For)
  • Review Redis key consistency

Implementation Files

  • Middleware: src/middleware/distributed-rate-limit.ts - Rate limiting implementation
  • Config: src/config/rate-limit.ts - Rate limit configuration
  • MCP Routes: src/routes/mcp.ts - MCP endpoint rate limiting
  • OAuth Routes: src/routes/oauth-register.ts - DCR endpoint rate limiting
  • Metrics: src/services/metrics.ts - Prometheus metrics definitions
  • Tests: src/middleware/distributed-rate-limit.test.ts

Released under the MIT License.