Skip to content

Enhanced Rate Limiting

Priority: LOW Estimated Time: 8-12 hours Status: Not Planned

← Back to Enhancements


Overview

Extend Seed's existing distributed rate limiting with per-user and per-tool granular controls. Currently, Seed implements IP-based rate limiting. This enhancement would add user-specific and tool-specific rate limits for finer control.


Current Implementation

Seed currently has distributed rate limiting (v0.1.3):

typescript
// src/middleware/distributed-rate-limit.ts
export const mcpRateLimit = createDistributedRateLimit({
  windowMs: 60 * 1000,        // 1 minute
  max: 100,                    // 100 requests per minute per IP
  keyPrefix: 'rate-limit:mcp'
});

Limitations:

  • Only IP-based limiting
  • Same limits for all users
  • Same limits for all tools
  • No burst allowance

Proposed Enhancements

1. Per-User Rate Limiting

Rate limit based on authenticated user ID:

typescript
interface UserRateLimits {
  global: number;          // Global requests per minute
  perTool: Record<string, number>;  // Per-tool limits
  burst?: number;          // Burst allowance
}

// Example: Premium users get higher limits
const userLimits: Record<string, UserRateLimits> = {
  'free-tier': { global: 50, perTool: {} },
  'premium': { global: 500, perTool: {} },
  'enterprise': { global: 5000, perTool: {} }
};

2. Per-Tool Rate Limiting

Different limits for different MCP tools:

typescript
const toolLimits: Record<string, number> = {
  'echo': 100,               // High limit for simple tools
  'system-status': 10,       // Low limit for expensive operations
  'healthcheck': 1000        // Very high for monitoring
};

3. Composite Keys

Combine multiple factors:

typescript
// Rate limit per user + tool
const key = `rate-limit:user:${userId}:tool:${toolName}`;

// Rate limit per user + IP (detect account sharing)
const key = `rate-limit:user:${userId}:ip:${ip}`;

Implementation

Note: Redis is included in the local development environment when using ./scripts/local (part of the Docker stack).

Rate Limit Service

Create src/services/rate-limit.ts:

typescript
import { Redis } from 'ioredis';

interface RateLimitConfig {
  global?: number;
  perUser?: number;
  perTool?: Record<string, number>;
  burstAllowance?: number;
}

export class RateLimitService {
  constructor(private redis: Redis, private config: RateLimitConfig) {}

  async checkLimit(userId: string, toolName?: string, ip?: string): Promise<{
    allowed: boolean;
    limit: number;
    remaining: number;
    resetAt: Date;
  }> {
    const checks = [];

    // Check global limit
    if (this.config.global) {
      checks.push(this.checkKey(`global:${ip}`, this.config.global));
    }

    // Check per-user limit
    if (this.config.perUser && userId) {
      checks.push(this.checkKey(`user:${userId}`, this.config.perUser));
    }

    // Check per-tool limit
    if (toolName && this.config.perTool?.[toolName]) {
      const limit = this.config.perTool[toolName];
      checks.push(this.checkKey(`user:${userId}:tool:${toolName}`, limit));
    }

    // All checks must pass
    const results = await Promise.all(checks);
    const blocked = results.find(r => !r.allowed);

    return blocked || results[0];
  }

  private async checkKey(key: string, limit: number): Promise<{
    allowed: boolean;
    limit: number;
    remaining: number;
    resetAt: Date;
  }> {
    const fullKey = `rate-limit:${key}`;
    const windowMs = 60000; // 1 minute

    // Increment counter with sliding window
    const pipeline = this.redis.pipeline();
    pipeline.incr(fullKey);
    pipeline.pexpire(fullKey, windowMs);
    const [[, count]] = await pipeline.exec();

    const current = count as number;
    const allowed = current <= limit;
    const remaining = Math.max(0, limit - current);
    const resetAt = new Date(Date.now() + windowMs);

    return { allowed, limit, remaining, resetAt };
  }
}

Middleware Integration

Update src/middleware/rate-limit.ts:

typescript
export function createEnhancedRateLimit(config: RateLimitConfig) {
  const service = new RateLimitService(redis, config);

  return async (req: Request, res: Response, next: NextFunction) => {
    const userId = req.userContext?.sub;
    const toolName = extractToolName(req);
    const ip = req.ip;

    const result = await service.checkLimit(userId, toolName, ip);

    // Set rate limit headers
    res.setHeader('X-RateLimit-Limit', result.limit);
    res.setHeader('X-RateLimit-Remaining', result.remaining);
    res.setHeader('X-RateLimit-Reset', result.resetAt.toISOString());

    if (!result.allowed) {
      return res.status(429).json({
        jsonrpc: '2.0',
        error: {
          code: -32007,
          message: 'Rate limit exceeded',
          data: {
            limit: result.limit,
            remaining: result.remaining,
            resetAt: result.resetAt
          }
        },
        id: null
      });
    }

    next();
  };
}

Configuration

Add to .env.example:

bash
# Enhanced Rate Limiting
RATE_LIMIT_GLOBAL=100                    # Global limit per IP
RATE_LIMIT_PER_USER=500                  # Per authenticated user
RATE_LIMIT_BURST_ALLOWANCE=20            # Burst allowance
RATE_LIMIT_TOOL_ECHO=1000                # Per-tool limits
RATE_LIMIT_TOOL_SYSTEM_STATUS=10

Load from environment:

typescript
// src/config/rate-limit.ts
export const rateLimitConfig = {
  global: parseInt(process.env.RATE_LIMIT_GLOBAL ?? '100'),
  perUser: parseInt(process.env.RATE_LIMIT_PER_USER ?? '500'),
  perTool: {
    'echo': parseInt(process.env.RATE_LIMIT_TOOL_ECHO ?? '1000'),
    'system-status': parseInt(process.env.RATE_LIMIT_TOOL_SYSTEM_STATUS ?? '10'),
  },
  burstAllowance: parseInt(process.env.RATE_LIMIT_BURST_ALLOWANCE ?? '20')
};

Advanced Features

Burst Allowance with Token Bucket

Allow short bursts above the steady-state limit:

typescript
class TokenBucketRateLimit {
  async checkLimit(key: string, rate: number, burst: number): Promise<boolean> {
    const now = Date.now();
    const bucketKey = `rate-limit:bucket:${key}`;

    // Get current tokens and last refill time
    const [tokens, lastRefill] = await this.redis.hmget(bucketKey, 'tokens', 'lastRefill');

    // Calculate refill
    const elapsed = now - (parseInt(lastRefill) || now);
    const refillAmount = (elapsed / 1000) * (rate / 60);
    const newTokens = Math.min(burst, (parseFloat(tokens) || burst) + refillAmount);

    // Check if we have a token
    if (newTokens >= 1) {
      await this.redis.hmset(bucketKey, {
        tokens: newTokens - 1,
        lastRefill: now
      });
      return true;
    }

    return false;
  }
}

Adaptive Rate Limiting

Adjust limits based on system load:

typescript
class AdaptiveRateLimit {
  async getAdjustedLimit(baseLimit: number): Promise<number> {
    // Check Redis memory usage
    const info = await this.redis.info('memory');
    const memoryUsagePercent = parseMemoryUsage(info);

    // Reduce limits if system is under pressure
    if (memoryUsagePercent > 90) {
      return Math.floor(baseLimit * 0.5);  // 50% reduction
    } else if (memoryUsagePercent > 80) {
      return Math.floor(baseLimit * 0.75); // 25% reduction
    }

    return baseLimit;
  }
}

User Tier Management

Assign users to tiers with different limits:

typescript
interface UserTier {
  name: string;
  limits: {
    requestsPerMinute: number;
    requestsPerHour: number;
    requestsPerDay: number;
    burstAllowance: number;
  };
}

const tiers: Record<string, UserTier> = {
  free: {
    name: 'Free',
    limits: {
      requestsPerMinute: 10,
      requestsPerHour: 100,
      requestsPerDay: 1000,
      burstAllowance: 5
    }
  },
  premium: {
    name: 'Premium',
    limits: {
      requestsPerMinute: 100,
      requestsPerHour: 5000,
      requestsPerDay: 50000,
      burstAllowance: 50
    }
  }
};

// Extract tier from JWT claims
function getUserTier(userContext: UserContext): UserTier {
  const tierName = userContext.groups?.includes('premium') ? 'premium' : 'free';
  return tiers[tierName];
}

Monitoring & Metrics

Add Prometheus metrics for rate limiting:

typescript
// src/services/metrics.ts
export const rateLimitHits = new promClient.Counter({
  name: 'rate_limit_hits_total',
  help: 'Total number of rate limit checks',
  labelNames: ['result', 'limit_type', 'user_tier']
});

export const rateLimitExceeded = new promClient.Counter({
  name: 'rate_limit_exceeded_total',
  help: 'Total number of rate limit violations',
  labelNames: ['limit_type', 'tool_name', 'user_tier']
});

export const rateLimitRemaining = new promClient.Gauge({
  name: 'rate_limit_remaining',
  help: 'Current remaining rate limit allowance',
  labelNames: ['limit_type', 'user_id']
});

Grafana Dashboard Queries

promql
# Rate limit hit rate by type
rate(rate_limit_hits_total[5m])

# Rate limit violations
rate(rate_limit_exceeded_total[5m])

# Users hitting rate limits frequently
topk(10, rate(rate_limit_exceeded_total{user_tier="free"}[1h]))

API Response Headers

Standard rate limit headers:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 2026-01-05T12:35:00Z
X-RateLimit-Tier: premium
X-RateLimit-Burst-Remaining: 45

When rate limited:

HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2026-01-05T12:35:42Z

Testing

Unit Tests

typescript
describe('Enhanced Rate Limiting', () => {
  it('should enforce per-user limits', async () => {
    const service = new RateLimitService(redis, { perUser: 10 });

    // Make 10 requests - all should succeed
    for (let i = 0; i < 10; i++) {
      const result = await service.checkLimit('user-123');
      expect(result.allowed).toBe(true);
    }

    // 11th request should fail
    const result = await service.checkLimit('user-123');
    expect(result.allowed).toBe(false);
  });

  it('should enforce per-tool limits', async () => {
    const service = new RateLimitService(redis, {
      perTool: { 'expensive-tool': 5 }
    });

    // Tool-specific limit should be enforced
    for (let i = 0; i < 5; i++) {
      const result = await service.checkLimit('user-123', 'expensive-tool');
      expect(result.allowed).toBe(true);
    }

    const result = await service.checkLimit('user-123', 'expensive-tool');
    expect(result.allowed).toBe(false);
  });
});

Migration Path

Phase 1: Add Enhanced Service (Optional)

typescript
// Make enhanced rate limiting opt-in
if (process.env.RATE_LIMIT_ENHANCED === 'true') {
  app.use('/mcp', enhancedRateLimit);
} else {
  app.use('/mcp', basicRateLimit);
}

Phase 2: Enable Gradually

  • Enable for specific users first
  • Monitor performance impact
  • Roll out to all users

Open Questions

  1. Limit Discovery: Should users be able to query their rate limits via API?
  2. Limit Increases: How should users request limit increases?
  3. Billing Integration: Should rate limits be tied to billing plans?
  4. Grace Period: Should first-time violators get a warning instead of hard block?

  • Multi-Tenancy Support - Per-tenant rate limits
  • Audit Logging - Track rate limit violations
  • Monitoring - Prometheus metrics integration

← Back to Enhancements

Released under the MIT License.