Skip to main content
Glama
cameronsjo

MCP Server Template

by cameronsjo
rate-limiter.ts5.62 kB
/** * Rate limiting with exponential backoff * * Provides per-source rate limiting to prevent API abuse. * Uses sliding window algorithm with configurable limits. */ import { createLogger } from './logger.js'; import { RateLimitError } from './errors.js'; const logger = createLogger('rate-limiter'); interface RateLimitConfig { /** Maximum requests allowed in the window */ requestsPerWindow: number; /** Window duration in milliseconds */ windowMs: number; } interface RateLimitState { requests: number[]; backoffUntil: number; consecutiveFailures: number; } const DEFAULT_CONFIG: RateLimitConfig = { requestsPerWindow: 60, windowMs: 60_000, // 1 minute }; /** * Rate limiter with per-source tracking and exponential backoff */ export class RateLimiter { private readonly configs = new Map<string, RateLimitConfig>(); private readonly state = new Map<string, RateLimitState>(); /** * Configure rate limits for a specific source */ configure(source: string, config: Partial<RateLimitConfig>): void { this.configs.set(source, { ...DEFAULT_CONFIG, ...config }); logger.debug('Configured rate limit', { source, ...this.configs.get(source) }); } /** * Get current config for a source */ getConfig(source: string): RateLimitConfig { return this.configs.get(source) ?? DEFAULT_CONFIG; } /** * Check if a request is allowed and record it */ async checkLimit(source: string): Promise<void> { const config = this.getConfig(source); const state = this.getState(source); const now = Date.now(); // Check backoff if (state.backoffUntil > now) { const retryAfter = Math.ceil((state.backoffUntil - now) / 1000); logger.warning('Rate limit backoff active', { source, retryAfter }); throw new RateLimitError(retryAfter); } // Clean old requests outside window const windowStart = now - config.windowMs; state.requests = state.requests.filter((time) => time > windowStart); // Check limit if (state.requests.length >= config.requestsPerWindow) { const oldestRequest = state.requests[0] ?? now; const retryAfter = Math.ceil((oldestRequest + config.windowMs - now) / 1000); logger.warning('Rate limit exceeded', { source, retryAfter }); throw new RateLimitError(retryAfter); } // Record request state.requests.push(now); } /** * Wait until a slot is available (non-blocking check) */ async waitForSlot(source: string): Promise<void> { const config = this.getConfig(source); const state = this.getState(source); const now = Date.now(); // Check backoff first if (state.backoffUntil > now) { const waitMs = state.backoffUntil - now; logger.debug('Waiting for backoff', { source, waitMs }); await this.sleep(waitMs); } // Clean old requests const windowStart = now - config.windowMs; state.requests = state.requests.filter((time) => time > windowStart); // Wait if at limit if (state.requests.length >= config.requestsPerWindow) { const oldestRequest = state.requests[0] ?? now; const waitMs = oldestRequest + config.windowMs - now + 100; // Small buffer logger.debug('Waiting for rate limit slot', { source, waitMs }); await this.sleep(waitMs); } // Record request state.requests.push(Date.now()); } /** * Record a failure and apply backoff */ recordFailure(source: string): void { const state = this.getState(source); state.consecutiveFailures++; // Exponential backoff: 2^failures seconds, max 60 seconds const backoffSeconds = Math.min(Math.pow(2, state.consecutiveFailures), 60); state.backoffUntil = Date.now() + backoffSeconds * 1000; logger.warning('Recorded failure, applying backoff', { source, consecutiveFailures: state.consecutiveFailures, backoffSeconds, }); } /** * Record a success and reset failure count */ recordSuccess(source: string): void { const state = this.getState(source); if (state.consecutiveFailures > 0) { logger.debug('Resetting failure count', { source }); state.consecutiveFailures = 0; state.backoffUntil = 0; } } /** * Get remaining requests in current window */ getRemainingRequests(source: string): number { const config = this.getConfig(source); const state = this.getState(source); const now = Date.now(); const windowStart = now - config.windowMs; const activeRequests = state.requests.filter((time) => time > windowStart).length; return Math.max(0, config.requestsPerWindow - activeRequests); } /** * Reset all rate limits (useful for testing) */ reset(): void { this.state.clear(); logger.debug('Reset all rate limits'); } private getState(source: string): RateLimitState { let state = this.state.get(source); if (!state) { state = { requests: [], backoffUntil: 0, consecutiveFailures: 0, }; this.state.set(source, state); } return state; } private sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } } // Global rate limiter instance let globalRateLimiter: RateLimiter | null = null; /** * Get the global rate limiter instance */ export function getRateLimiter(): RateLimiter { if (!globalRateLimiter) { globalRateLimiter = new RateLimiter(); } return globalRateLimiter; } /** * Reset the global rate limiter (for testing) */ export function resetRateLimiter(): void { globalRateLimiter = null; }

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/cameronsjo/mcp-server-template'

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