Claude Code Patterns for Handling Rate Limits and Quotas at Scale

Overview

Managing rate limits and quotas is critical for production Claude Code deployments. This guide provides comprehensive patterns for handling API rate limits, implementing quota management systems, and scaling strategies for high-volume applications.

Understanding Claude API Rate Limits

Rate Limit Types

  1. Requests Per Minute (RPM): Limits API calls within a 60-second window
  2. Tokens Per Minute (TPM): Caps total tokens processed per minute
  3. Daily Token Quota: Restricts total tokens within 24 hours

Tier-Based Limits

interface ClaudeRateLimits {
  free: {
    rpm: 60,
    tpm: 40000,
    dailyTokens: 100000
  },
  pro: {
    rpm: 300,
    tpm: 200000,
    dailyTokens: 500000
  },
  enterprise: {
    rpm: 'custom',
    tpm: 'custom',
    dailyTokens: 'custom'
  }
}

Rate Limiting Patterns

1. Token Bucket Algorithm

class TokenBucket {
  private tokens: number;
  private capacity: number;
  private refillRate: number;
  private lastRefill: number;
 
  constructor(capacity: number, refillRate: number) {
    this.capacity = capacity;
    this.tokens = capacity;
    this.refillRate = refillRate;
    this.lastRefill = Date.now();
  }
 
  async consume(tokensNeeded: number): Promise<boolean> {
    this.refill();
    
    if (this.tokens >= tokensNeeded) {
      this.tokens -= tokensNeeded;
      return true;
    }
    
    return false;
  }
 
  private refill(): void {
    const now = Date.now();
    const timePassed = (now - this.lastRefill) / 1000;
    const tokensToAdd = timePassed * this.refillRate;
    
    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }
 
  getAvailableTokens(): number {
    this.refill();
    return Math.floor(this.tokens);
  }
}
 
// Usage
const rateLimiter = new TokenBucket(300, 5); // 300 tokens, 5 per second

2. Sliding Window Rate Limiter

class SlidingWindowRateLimiter {
  private requests: number[] = [];
  private windowMs: number;
  private maxRequests: number;
 
  constructor(maxRequests: number, windowMs: number) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
  }
 
  async checkLimit(): Promise<boolean> {
    const now = Date.now();
    const windowStart = now - this.windowMs;
    
    // Remove requests outside the window
    this.requests = this.requests.filter(time => time > windowStart);
    
    if (this.requests.length < this.maxRequests) {
      this.requests.push(now);
      return true;
    }
    
    return false;
  }
 
  getNextAvailableTime(): number {
    if (this.requests.length < this.maxRequests) {
      return Date.now();
    }
    
    const oldestRequest = Math.min(...this.requests);
    return oldestRequest + this.windowMs;
  }
}

3. Circuit Breaker Pattern

import CircuitBreaker from 'opossum';
 
class ClaudeAPICircuitBreaker {
  private breaker: CircuitBreaker;
 
  constructor() {
    const options = {
      timeout: 30000, // 30 seconds
      errorThresholdPercentage: 50,
      resetTimeout: 30000,
      requestVolumeThreshold: 10
    };
 
    this.breaker = new CircuitBreaker(this.callClaudeAPI, options);
    
    this.breaker.on('open', () => {
      console.log('Circuit breaker is open - API calls blocked');
    });
    
    this.breaker.on('halfOpen', () => {
      console.log('Circuit breaker is half-open - testing API');
    });
    
    this.breaker.on('close', () => {
      console.log('Circuit breaker is closed - normal operation');
    });
  }
 
  async makeRequest(prompt: string): Promise<any> {
    try {
      return await this.breaker.fire(prompt);
    } catch (error) {
      if (error.code === 'EOPENBREAKER') {
        throw new Error('Service temporarily unavailable');
      }
      throw error;
    }
  }
 
  private async callClaudeAPI(prompt: string): Promise<any> {
    // Actual API call implementation
    return await claudeCode({ prompt });
  }
}

Multi-API Key Management

1. Round-Robin Key Distribution

class APIKeyManager {
  private keys: string[];
  private currentIndex: number = 0;
  private keyUsage: Map<string, TokenBucket>;
 
  constructor(apiKeys: string[]) {
    this.keys = apiKeys;
    this.keyUsage = new Map();
    
    // Initialize rate limiter for each key
    apiKeys.forEach(key => {
      this.keyUsage.set(key, new TokenBucket(300, 5));
    });
  }
 
  async getNextAvailableKey(): Promise<string | null> {
    const startIndex = this.currentIndex;
    
    do {
      const key = this.keys[this.currentIndex];
      const bucket = this.keyUsage.get(key)!;
      
      if (bucket.getAvailableTokens() > 0) {
        this.currentIndex = (this.currentIndex + 1) % this.keys.length;
        return key;
      }
      
      this.currentIndex = (this.currentIndex + 1) % this.keys.length;
    } while (this.currentIndex !== startIndex);
    
    return null; // All keys exhausted
  }
}

2. Weighted Key Selection

interface WeightedAPIKey {
  key: string;
  weight: number;
  tier: 'free' | 'pro' | 'enterprise';
}
 
class WeightedAPIKeyManager {
  private keys: WeightedAPIKey[];
  private rateLimiters: Map<string, TokenBucket>;
 
  constructor(keys: WeightedAPIKey[]) {
    this.keys = keys;
    this.rateLimiters = new Map();
    
    keys.forEach(({ key, tier }) => {
      const limits = this.getTierLimits(tier);
      this.rateLimiters.set(key, new TokenBucket(limits.rpm, limits.rpm / 60));
    });
  }
 
  async selectKey(): Promise<string | null> {
    // Sort keys by available capacity and weight
    const availableKeys = this.keys
      .filter(({ key }) => {
        const limiter = this.rateLimiters.get(key)!;
        return limiter.getAvailableTokens() > 0;
      })
      .sort((a, b) => {
        const aTokens = this.rateLimiters.get(a.key)!.getAvailableTokens();
        const bTokens = this.rateLimiters.get(b.key)!.getAvailableTokens();
        return (bTokens * b.weight) - (aTokens * a.weight);
      });
 
    return availableKeys.length > 0 ? availableKeys[0].key : null;
  }
 
  private getTierLimits(tier: string) {
    const limits = {
      free: { rpm: 60, tpm: 40000 },
      pro: { rpm: 300, tpm: 200000 },
      enterprise: { rpm: 1000, tpm: 1000000 }
    };
    return limits[tier] || limits.free;
  }
}

Quota Management System

1. User-Based Quota Tracking

interface UserQuota {
  userId: string;
  dailyTokenLimit: number;
  monthlyTokenLimit: number;
  tokensUsedToday: number;
  tokensUsedThisMonth: number;
  lastResetDaily: Date;
  lastResetMonthly: Date;
}
 
class QuotaManager {
  private quotas: Map<string, UserQuota> = new Map();
  private storage: QuotaStorage;
 
  constructor(storage: QuotaStorage) {
    this.storage = storage;
  }
 
  async checkAndConsumeQuota(
    userId: string, 
    tokensNeeded: number
  ): Promise<{ allowed: boolean; reason?: string }> {
    const quota = await this.getUserQuota(userId);
    
    // Reset if needed
    this.resetQuotaIfNeeded(quota);
    
    // Check daily limit
    if (quota.tokensUsedToday + tokensNeeded > quota.dailyTokenLimit) {
      return { 
        allowed: false, 
        reason: 'Daily token limit exceeded' 
      };
    }
    
    // Check monthly limit
    if (quota.tokensUsedThisMonth + tokensNeeded > quota.monthlyTokenLimit) {
      return { 
        allowed: false, 
        reason: 'Monthly token limit exceeded' 
      };
    }
    
    // Consume tokens
    quota.tokensUsedToday += tokensNeeded;
    quota.tokensUsedThisMonth += tokensNeeded;
    
    await this.storage.updateQuota(quota);
    
    return { allowed: true };
  }
 
  private resetQuotaIfNeeded(quota: UserQuota): void {
    const now = new Date();
    
    // Reset daily
    if (this.isDifferentDay(quota.lastResetDaily, now)) {
      quota.tokensUsedToday = 0;
      quota.lastResetDaily = now;
    }
    
    // Reset monthly
    if (this.isDifferentMonth(quota.lastResetMonthly, now)) {
      quota.tokensUsedThisMonth = 0;
      quota.lastResetMonthly = now;
    }
  }
 
  private isDifferentDay(date1: Date, date2: Date): boolean {
    return date1.toDateString() !== date2.toDateString();
  }
 
  private isDifferentMonth(date1: Date, date2: Date): boolean {
    return date1.getMonth() !== date2.getMonth() || 
           date1.getFullYear() !== date2.getFullYear();
  }
}

2. Organization-Level Quota Pooling

class OrganizationQuotaPool {
  private orgQuotas: Map<string, OrganizationQuota> = new Map();
  
  async allocateFromPool(
    orgId: string,
    userId: string,
    tokensNeeded: number
  ): Promise<AllocationResult> {
    const org = await this.getOrgQuota(orgId);
    
    // Check if organization has enough tokens
    if (org.availableTokens < tokensNeeded) {
      // Try to purchase more tokens automatically
      if (org.autoPurchaseEnabled) {
        await this.purchaseAdditionalTokens(orgId, tokensNeeded);
      } else {
        return {
          success: false,
          reason: 'Organization token pool exhausted'
        };
      }
    }
    
    // Allocate tokens to user
    org.availableTokens -= tokensNeeded;
    org.userAllocations.set(userId, 
      (org.userAllocations.get(userId) || 0) + tokensNeeded
    );
    
    await this.saveOrgQuota(org);
    
    return { success: true, tokensAllocated: tokensNeeded };
  }
 
  private async purchaseAdditionalTokens(
    orgId: string, 
    minimumNeeded: number
  ): Promise<void> {
    // Calculate purchase amount (buy in blocks)
    const blockSize = 1000000; // 1M tokens
    const blocksToPurchase = Math.ceil(minimumNeeded / blockSize);
    
    // Implement payment processing
    await this.processPayment(orgId, blocksToPurchase);
    
    // Update organization quota
    const org = await this.getOrgQuota(orgId);
    org.availableTokens += blocksToPurchase * blockSize;
    await this.saveOrgQuota(org);
  }
}

Advanced Scaling Strategies

1. Request Queuing with Priority

class PriorityRequestQueue {
  private queues: Map<Priority, RequestItem[]> = new Map();
  private processing: boolean = false;
  
  constructor(private rateLimiter: RateLimiter) {
    // Initialize priority queues
    ['high', 'medium', 'low'].forEach(priority => {
      this.queues.set(priority as Priority, []);
    });
  }
 
  async enqueue(
    request: ClaudeRequest, 
    priority: Priority = 'medium'
  ): Promise<ClaudeResponse> {
    return new Promise((resolve, reject) => {
      const item: RequestItem = {
        request,
        priority,
        timestamp: Date.now(),
        resolve,
        reject
      };
      
      this.queues.get(priority)!.push(item);
      
      // Start processing if not already running
      if (!this.processing) {
        this.processQueue();
      }
    });
  }
 
  private async processQueue(): Promise<void> {
    this.processing = true;
    
    while (this.hasRequests()) {
      // Get highest priority request
      const item = this.getNextRequest();
      if (!item) break;
      
      // Wait for rate limit availability
      while (!await this.rateLimiter.checkLimit()) {
        await this.sleep(100); // Check every 100ms
      }
      
      try {
        const response = await this.executeRequest(item.request);
        item.resolve(response);
      } catch (error) {
        item.reject(error);
      }
    }
    
    this.processing = false;
  }
 
  private getNextRequest(): RequestItem | null {
    // Check queues in priority order
    for (const priority of ['high', 'medium', 'low'] as Priority[]) {
      const queue = this.queues.get(priority)!;
      if (queue.length > 0) {
        return queue.shift()!;
      }
    }
    return null;
  }
 
  private hasRequests(): boolean {
    return Array.from(this.queues.values()).some(q => q.length > 0);
  }
}

2. Adaptive Rate Limiting

class AdaptiveRateLimiter {
  private successRate: number = 1.0;
  private adjustmentFactor: number = 0.1;
  private minRate: number = 0.1;
  private maxRate: number = 1.0;
  
  constructor(
    private baseRateLimiter: RateLimiter,
    private metricsCollector: MetricsCollector
  ) {}
 
  async executeWithAdaptiveLimit<T>(
    operation: () => Promise<T>
  ): Promise<T> {
    const shouldProceed = Math.random() < this.successRate;
    
    if (!shouldProceed) {
      await this.sleep(1000); // Back off
      return this.executeWithAdaptiveLimit(operation);
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      if (this.isRateLimitError(error)) {
        this.onRateLimit();
      }
      throw error;
    }
  }
 
  private onSuccess(): void {
    // Gradually increase success rate
    this.successRate = Math.min(
      this.maxRate,
      this.successRate + this.adjustmentFactor * 0.1
    );
    
    this.metricsCollector.recordSuccess();
  }
 
  private onRateLimit(): void {
    // Aggressively decrease success rate
    this.successRate = Math.max(
      this.minRate,
      this.successRate - this.adjustmentFactor
    );
    
    this.metricsCollector.recordRateLimit();
  }
 
  private isRateLimitError(error: any): boolean {
    return error.status === 429 || 
           error.code === 'rate_limit_exceeded';
  }
}

3. Distributed Rate Limiting with Redis

class DistributedRateLimiter {
  constructor(
    private redis: Redis,
    private limits: RateLimitConfig
  ) {}
 
  async checkAndConsumeLimit(
    key: string,
    tokensNeeded: number = 1
  ): Promise<RateLimitResult> {
    const script = `
      local key = KEYS[1]
      local limit = tonumber(ARGV[1])
      local window = tonumber(ARGV[2])
      local tokens_needed = tonumber(ARGV[3])
      local now = tonumber(ARGV[4])
      
      local current = redis.call('GET', key)
      if current == false then
        current = 0
      else
        current = tonumber(current)
      end
      
      if current + tokens_needed > limit then
        return {0, limit - current}
      end
      
      redis.call('INCRBY', key, tokens_needed)
      redis.call('EXPIRE', key, window)
      
      return {1, limit - current - tokens_needed}
    `;
 
    const result = await this.redis.eval(
      script,
      1,
      key,
      this.limits.maxTokens,
      this.limits.windowSeconds,
      tokensNeeded,
      Date.now()
    ) as [number, number];
 
    return {
      allowed: result[0] === 1,
      remainingTokens: result[1],
      resetTime: Date.now() + (this.limits.windowSeconds * 1000)
    };
  }
}

Production Best Practices

1. Monitoring and Alerting

class RateLimitMonitor {
  private metrics: {
    totalRequests: number;
    rateLimitHits: number;
    avgWaitTime: number;
    quotaExhausted: number;
  } = {
    totalRequests: 0,
    rateLimitHits: 0,
    avgWaitTime: 0,
    quotaExhausted: 0
  };
 
  recordRequest(result: RequestResult): void {
    this.metrics.totalRequests++;
    
    if (result.rateLimited) {
      this.metrics.rateLimitHits++;
    }
    
    if (result.waitTime) {
      this.updateAvgWaitTime(result.waitTime);
    }
    
    // Alert if rate limit hit rate is too high
    if (this.getRateLimitHitRate() > 0.1) {
      this.sendAlert('High rate limit hit rate detected');
    }
  }
 
  private getRateLimitHitRate(): number {
    if (this.metrics.totalRequests === 0) return 0;
    return this.metrics.rateLimitHits / this.metrics.totalRequests;
  }
 
  private sendAlert(message: string): void {
    // Send to monitoring service
    console.error(`ALERT: ${message}`, {
      metrics: this.metrics,
      timestamp: new Date().toISOString()
    });
  }
}

2. Graceful Degradation

class GracefulDegradationHandler {
  async executeWithFallback<T>(
    primary: () => Promise<T>,
    fallbacks: Array<() => Promise<T>>
  ): Promise<T> {
    try {
      return await primary();
    } catch (error) {
      if (this.isRateLimitError(error)) {
        // Try fallback strategies
        for (const fallback of fallbacks) {
          try {
            return await fallback();
          } catch (fallbackError) {
            continue;
          }
        }
      }
      throw error;
    }
  }
 
  // Example usage
  async processRequest(prompt: string): Promise<any> {
    return this.executeWithFallback(
      // Primary: Use Claude Opus
      () => this.callClaude(prompt, 'opus'),
      [
        // Fallback 1: Use Claude Sonnet
        () => this.callClaude(prompt, 'sonnet'),
        // Fallback 2: Use cached response
        () => this.getCachedResponse(prompt),
        // Fallback 3: Return degraded response
        () => this.getDegradedResponse(prompt)
      ]
    );
  }
}

3. Cost Optimization

class CostOptimizer {
  private costPerToken = {
    'claude-4-opus': 0.00015,
    'claude-3.5-sonnet': 0.00003,
    'claude-3-haiku': 0.0000025
  };
 
  async selectModelBasedOnBudget(
    prompt: string,
    maxCost: number
  ): Promise<ModelSelection> {
    const estimatedTokens = this.estimateTokens(prompt);
    
    // Find the best model within budget
    for (const [model, cost] of Object.entries(this.costPerToken)) {
      const totalCost = estimatedTokens * cost;
      if (totalCost <= maxCost) {
        return {
          model,
          estimatedCost: totalCost,
          estimatedTokens
        };
      }
    }
    
    throw new Error('No model available within budget');
  }
 
  private estimateTokens(text: string): number {
    // Rough estimation: 1 token ≈ 4 characters
    return Math.ceil(text.length / 4);
  }
}

Error Handling and Recovery

1. Exponential Backoff with Jitter

class ExponentialBackoff {
  constructor(
    private baseDelay: number = 1000,
    private maxDelay: number = 30000,
    private factor: number = 2
  ) {}
 
  async executeWithRetry<T>(
    operation: () => Promise<T>,
    maxRetries: number = 5
  ): Promise<T> {
    let lastError: any;
    
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        
        if (!this.isRetryable(error)) {
          throw error;
        }
        
        const delay = this.calculateDelay(attempt);
        await this.sleep(delay);
      }
    }
    
    throw lastError;
  }
 
  private calculateDelay(attempt: number): number {
    // Exponential backoff with jitter
    const exponentialDelay = Math.min(
      this.baseDelay * Math.pow(this.factor, attempt),
      this.maxDelay
    );
    
    // Add jitter (±25%)
    const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
    
    return Math.floor(exponentialDelay + jitter);
  }
 
  private isRetryable(error: any): boolean {
    // Retry on rate limits and temporary errors
    return error.status === 429 || 
           error.status === 503 ||
           error.code === 'ETIMEDOUT';
  }
}

Conclusion

Effective rate limit and quota management is essential for production Claude Code deployments. By implementing these patterns, you can build resilient, scalable applications that gracefully handle API limits while optimizing costs and maintaining high availability.

Last verified: 2025-07-21
Source: Web research and production deployment patterns