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
- Requests Per Minute (RPM): Limits API calls within a 60-second window
- Tokens Per Minute (TPM): Caps total tokens processed per minute
- 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 second2. 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.
Related Resources
Last verified: 2025-07-21
Source: Web research and production deployment patterns