Skip to main content
Glama
rate-limiter.ts7.67 kB
/** * Rate Limiter - Token bucket-based rate limiting */ export interface RateLimitConfig { maxTokens: number; // Maximum number of tokens in bucket refillRate: number; // Tokens added per second windowMs?: number; // Optional window for burst control } export interface RateLimitResult { allowed: boolean; remaining: number; resetAt: number; retryAfter?: number; } interface TokenBucket { tokens: number; lastRefill: number; maxTokens: number; refillRate: number; } export class RateLimiter { private buckets: Map<string, TokenBucket>; private cleanupInterval: NodeJS.Timeout | null = null; constructor() { this.buckets = new Map(); this.startCleanup(); } /** * Check if a request is allowed under rate limits */ checkLimit( identifier: string, config: RateLimitConfig, cost: number = 1 ): RateLimitResult { let bucket = this.buckets.get(identifier); // Create bucket if it doesn't exist if (!bucket) { bucket = { tokens: config.maxTokens, lastRefill: Date.now(), maxTokens: config.maxTokens, refillRate: config.refillRate, }; this.buckets.set(identifier, bucket); } // Refill tokens based on time elapsed this.refillBucket(bucket); // Check if enough tokens available if (bucket.tokens >= cost) { bucket.tokens -= cost; return { allowed: true, remaining: Math.floor(bucket.tokens), resetAt: this.calculateResetTime(bucket), }; } // Not enough tokens - rate limited const tokensNeeded = cost - bucket.tokens; const retryAfterMs = (tokensNeeded / bucket.refillRate) * 1000; return { allowed: false, remaining: 0, resetAt: this.calculateResetTime(bucket), retryAfter: Math.ceil(retryAfterMs / 1000), // seconds }; } /** * Refill tokens in a bucket based on elapsed time */ private refillBucket(bucket: TokenBucket): void { const now = Date.now(); const elapsedMs = now - bucket.lastRefill; const elapsedSeconds = elapsedMs / 1000; // Calculate tokens to add const tokensToAdd = elapsedSeconds * bucket.refillRate; // Add tokens up to max bucket.tokens = Math.min(bucket.maxTokens, bucket.tokens + tokensToAdd); bucket.lastRefill = now; } /** * Calculate when the bucket will be fully refilled */ private calculateResetTime(bucket: TokenBucket): number { const tokensToFill = bucket.maxTokens - bucket.tokens; const secondsToFill = tokensToFill / bucket.refillRate; return Date.now() + secondsToFill * 1000; } /** * Get remaining tokens for an identifier */ getRemainingTokens(identifier: string): number { const bucket = this.buckets.get(identifier); if (!bucket) { return 0; } this.refillBucket(bucket); return Math.floor(bucket.tokens); } /** * Reset rate limits for an identifier */ reset(identifier: string): void { this.buckets.delete(identifier); } /** * Reset all rate limits */ resetAll(): void { this.buckets.clear(); } /** * Get statistics for all rate limiters */ getStats(): { totalIdentifiers: number; identifiers: { identifier: string; remaining: number; maxTokens: number; refillRate: number; }[]; } { const identifiers = Array.from(this.buckets.entries()).map(([identifier, bucket]) => { this.refillBucket(bucket); return { identifier, remaining: Math.floor(bucket.tokens), maxTokens: bucket.maxTokens, refillRate: bucket.refillRate, }; }); return { totalIdentifiers: this.buckets.size, identifiers, }; } /** * Start automatic cleanup of inactive buckets */ private startCleanup(): void { // Clean up buckets older than 1 hour every 10 minutes this.cleanupInterval = setInterval(() => { this.cleanup(); }, 600000); // 10 minutes } /** * Stop automatic cleanup */ stopCleanup(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } /** * Clean up old buckets */ private cleanup(): void { const now = Date.now(); const maxAge = 3600000; // 1 hour const keysToDelete: string[] = []; for (const [identifier, bucket] of this.buckets.entries()) { // If bucket hasn't been accessed in over an hour and is full, remove it if ( now - bucket.lastRefill > maxAge && bucket.tokens >= bucket.maxTokens ) { keysToDelete.push(identifier); } } for (const key of keysToDelete) { this.buckets.delete(key); } if (keysToDelete.length > 0) { console.error(`Rate limiter cleanup: removed ${keysToDelete.length} inactive buckets`); } } } /** * Multi-tier rate limiter with different limits for different tiers */ export class TieredRateLimiter { private limiter: RateLimiter; private tierConfigs: Map<string, RateLimitConfig>; constructor() { this.limiter = new RateLimiter(); this.tierConfigs = new Map(); } /** * Set rate limit config for a tier */ setTierConfig(tier: string, config: RateLimitConfig): void { this.tierConfigs.set(tier, config); } /** * Check rate limit for a specific tier */ checkLimit( identifier: string, tier: string, cost: number = 1 ): RateLimitResult { const config = this.tierConfigs.get(tier); if (!config) { throw new Error(`Rate limit tier not configured: ${tier}`); } const bucketKey = `${tier}:${identifier}`; return this.limiter.checkLimit(bucketKey, config, cost); } /** * Get remaining tokens for an identifier in a tier */ getRemainingTokens(identifier: string, tier: string): number { const bucketKey = `${tier}:${identifier}`; return this.limiter.getRemainingTokens(bucketKey); } /** * Reset limits for an identifier in a tier */ reset(identifier: string, tier?: string): void { if (tier) { const bucketKey = `${tier}:${identifier}`; this.limiter.reset(bucketKey); } else { // Reset all tiers for this identifier for (const tierName of this.tierConfigs.keys()) { const bucketKey = `${tierName}:${identifier}`; this.limiter.reset(bucketKey); } } } /** * Get statistics */ getStats() { return this.limiter.getStats(); } /** * Stop cleanup */ stop(): void { this.limiter.stopCleanup(); } } // Global singleton instance let globalRateLimiter: TieredRateLimiter | null = null; /** * Get the global rate limiter instance */ export function getRateLimiter(): TieredRateLimiter { if (!globalRateLimiter) { globalRateLimiter = new TieredRateLimiter(); // Set default tiers globalRateLimiter.setTierConfig('default', { maxTokens: 100, refillRate: 10, // 10 requests per second }); globalRateLimiter.setTierConfig('authenticated', { maxTokens: 1000, refillRate: 50, // 50 requests per second }); globalRateLimiter.setTierConfig('premium', { maxTokens: 5000, refillRate: 200, // 200 requests per second }); } return globalRateLimiter; } /** * Initialize the global rate limiter with custom tiers */ export function initializeRateLimiter( tiers: Map<string, RateLimitConfig> ): TieredRateLimiter { const limiter = new TieredRateLimiter(); for (const [tier, config] of tiers) { limiter.setTierConfig(tier, config); } globalRateLimiter = limiter; return limiter; }

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/krtw00/search-mcp'

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