Skip to main content
Glama
rate-limiter.ts6.16 kB
/** * Rate Limiter * * Implements rate limiting to prevent abuse and ensure fair resource usage. * Uses a sliding window algorithm for accurate rate limiting. * * Requirements: 11.1, 11.2, 11.3, 11.4, 11.5 */ import type { RateLimitConfig, RateLimitContext, RateLimitEntry, RateLimitResult, } from "./types.js"; /** * Default rate limit configuration */ const DEFAULT_CONFIG: Required<RateLimitConfig> = { windowMs: 60000, // 1 minute maxRequests: 100, // 100 requests per minute keyGenerator: (ctx) => ctx.userId ?? ctx.ip ?? "anonymous", skipFailedRequests: false, skipSuccessfulRequests: false, }; /** * Rate Limiter class * * Implements sliding window rate limiting with configurable options */ export class RateLimiter { private config: Required<RateLimitConfig>; private entries: Map<string, RateLimitEntry>; private cleanupInterval: ReturnType<typeof setInterval> | null = null; constructor(config: Partial<RateLimitConfig> = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; this.entries = new Map(); // Start cleanup interval to prevent memory leaks this.startCleanup(); } /** * Check if a request is allowed under rate limits */ check(context: RateLimitContext): RateLimitResult { const key = this.config.keyGenerator(context); const now = Date.now(); const windowStart = now - this.config.windowMs; let entry = this.entries.get(key); // If no entry or entry is from a previous window, create new entry if (!entry || entry.windowStart < windowStart) { entry = { count: 0, windowStart: now, lastRequest: now, }; } // Calculate remaining requests const remaining = Math.max(0, this.config.maxRequests - entry.count); const resetTime = new Date(entry.windowStart + this.config.windowMs); // Check if rate limit exceeded if (entry.count >= this.config.maxRequests) { const retryAfter = Math.ceil((entry.windowStart + this.config.windowMs - now) / 1000); return { allowed: false, remaining: 0, resetTime, retryAfter: Math.max(1, retryAfter), }; } return { allowed: true, remaining: remaining - 1, // Account for this request resetTime, }; } /** * Record a request (increment counter) */ record(context: RateLimitContext, success: boolean = true): void { // Skip recording based on configuration if (this.config.skipFailedRequests && !success) { return; } if (this.config.skipSuccessfulRequests && success) { return; } const key = this.config.keyGenerator(context); const now = Date.now(); const windowStart = now - this.config.windowMs; let entry = this.entries.get(key); // If no entry or entry is from a previous window, create new entry if (!entry || entry.windowStart < windowStart) { entry = { count: 1, windowStart: now, lastRequest: now, }; } else { entry.count++; entry.lastRequest = now; } this.entries.set(key, entry); } /** * Check and record in one operation */ consume(context: RateLimitContext): RateLimitResult { const result = this.check(context); if (result.allowed) { this.record(context); } return result; } /** * Reset rate limit for a specific key */ reset(context: RateLimitContext): void { const key = this.config.keyGenerator(context); this.entries.delete(key); } /** * Reset all rate limits */ resetAll(): void { this.entries.clear(); } /** * Get current stats for a key */ getStats(context: RateLimitContext): { count: number; remaining: number; resetTime: Date; } | null { const key = this.config.keyGenerator(context); const entry = this.entries.get(key); if (!entry) { return null; } const now = Date.now(); const windowStart = now - this.config.windowMs; // Check if entry is still valid if (entry.windowStart < windowStart) { return null; } return { count: entry.count, remaining: Math.max(0, this.config.maxRequests - entry.count), resetTime: new Date(entry.windowStart + this.config.windowMs), }; } /** * Get total number of tracked keys */ getTrackedCount(): number { return this.entries.size; } /** * Start cleanup interval */ private startCleanup(): void { // Clean up expired entries every minute this.cleanupInterval = setInterval(() => { this.cleanup(); }, 60000); // Prevent interval from keeping process alive if (this.cleanupInterval.unref) { this.cleanupInterval.unref(); } } /** * Clean up expired entries */ private cleanup(): void { const now = Date.now(); const windowStart = now - this.config.windowMs; for (const [key, entry] of this.entries) { if (entry.windowStart < windowStart) { this.entries.delete(key); } } } /** * Stop the rate limiter and cleanup */ stop(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.entries.clear(); } } /** * Create a rate limiter with default configuration */ export function createRateLimiter(config?: RateLimitConfig): RateLimiter { return new RateLimiter(config); } /** * Create a rate limiter for MCP tools * Default: 100 requests per minute per user */ export function createToolRateLimiter(maxRequestsPerMinute: number = 100): RateLimiter { return new RateLimiter({ windowMs: 60000, maxRequests: maxRequestsPerMinute, keyGenerator: (ctx) => `tool:${ctx.userId ?? "anonymous"}:${ctx.toolName ?? "default"}`, }); } /** * Create a rate limiter for memory operations * Default: 50 requests per minute per user */ export function createMemoryRateLimiter(maxRequestsPerMinute: number = 50): RateLimiter { return new RateLimiter({ windowMs: 60000, maxRequests: maxRequestsPerMinute, keyGenerator: (ctx) => `memory:${ctx.userId ?? "anonymous"}`, }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server