DocsGuidesRate Limiting and Error Handling
Intermediate
8 min read

Rate Limiting and Error Handling

Implement robust error handling and manage rate limits effectively

By Synqly TeamUpdated December 2025

Real systems hit rate limits, timeouts, and provider outages. Your job is to make that invisible to users. This guide walks through: • What rate limits mean in practice • How to retry safely (exponential backoff + jitter) • Which errors are safe to retry • Circuit breaker patterns for stability • Observability—so you can debug incidents fast

Understanding Rate Limits

API providers impose rate limits to ensure fair usage: • Requests per minute (RPM) • Tokens per minute (TPM) • Tokens per day (TPD) • Concurrent requests Synqly aggregates these limits and provides unified rate limit headers in responses.

Implementing Exponential Backoff

Retry failed requests with increasing delays:

TypeScript
async function makeRequestWithRetry(
  fn: () => Promise<any>,
  maxRetries = 3
) {
  let lastError;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error: any) {
      lastError = error;
      
      // Don't retry on client errors (4xx except 429)
      if (error.status >= 400 && error.status < 500 && error.status !== 429) {
        throw error;
      }
      
      if (attempt < maxRetries) {
        // Calculate delay: 1s, 2s, 4s, 8s, etc.
        const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
        
        // Add jitter to prevent thundering herd
        const jitter = Math.random() * 1000;
        
        await new Promise(resolve => setTimeout(resolve, delay + jitter));
      }
    }
  }
  
  throw lastError;
}

Handling Different Error Types

Respond appropriately to different error scenarios:

TypeScript
async function handleAPICall(request: any) {
  try {
    return await synqly.chat.completions.create(request);
  } catch (error: any) {
    switch (error.status) {
      case 400:
        console.error('Invalid request:', error.message);
        throw new Error('Please check your input');
        
      case 401:
        console.error('Authentication failed');
        throw new Error('Invalid API key');
        
      case 429:
        const retryAfter = error.headers?.['retry-after'];
        console.log(`Rate limited. Retry after ${retryAfter}s`);
        throw new Error('Too many requests');
        
      case 500:
      case 502:
      case 503:
        console.error('Server error:', error.status);
        throw new Error('Service temporarily unavailable');
        
      default:
        console.error('Unexpected error:', error);
        throw new Error('An unexpected error occurred');
    }
  }
}

Circuit Breaker Pattern

Prevent cascading failures:

TypeScript
class CircuitBreaker {
  private failures = 0;
  private lastFailureTime: number | null = null;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  
  constructor(
    private threshold = 5,
    private timeout = 60000
  ) {}
  
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime! > this.timeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is open');
      }
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    
    if (this.failures >= this.threshold) {
      this.state = 'open';
    }
  }
}