Enhanced Rate Limiting
Priority: LOW Estimated Time: 8-12 hours Status: Not Planned
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):
// 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:
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:
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:
// 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:
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:
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:
# 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=10Load from environment:
// 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:
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:
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:
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:
// 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
# 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: 45When 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:42ZTesting
Unit Tests
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)
// 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
- Limit Discovery: Should users be able to query their rate limits via API?
- Limit Increases: How should users request limit increases?
- Billing Integration: Should rate limits be tied to billing plans?
- Grace Period: Should first-time violators get a warning instead of hard block?
Related Enhancements
- Multi-Tenancy Support - Per-tenant rate limits
- Audit Logging - Track rate limit violations
- Monitoring - Prometheus metrics integration