DevSecOps

Erori de Rate Limiting API: Cum să Gestionezi 429 Too Many Requests

Nicu Constantin
--7 min lectura
#api#rate-limiting#429-errors#troubleshooting#integration

Rate limiting-ul este esențial pentru stabilitatea API-urilor, dar devine frustrant când atingi limitele. Acest ghid acoperă cum să gestionezi limitele de rată în mod elegant și cum să optimizezi utilizarea API-urilor.

Înțelegerea Erorilor de Rate Limit

Răspunsuri frecvente:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Try again in 60 seconds.",
  "retry_after": 60
}

Headere de rate limit (variază în funcție de API):

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705680000
Retry-After: 60

Soluția 1: Exponential Backoff

Implementare JavaScript/Node.js:

async function fetchWithRetry(url, options = {}, maxRetries = 5) {
  let lastError;
 
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
 
      // Check rate limit headers
      const remaining = response.headers.get('X-RateLimit-Remaining');
      if (remaining && parseInt(remaining) < 10) {
        console.warn(`Rate limit warning: ${remaining} requests remaining`);
      }
 
      if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After');
        const waitTime = retryAfter
          ? parseInt(retryAfter) * 1000
          : Math.min(1000 * Math.pow(2, attempt), 60000); // Max 60s
 
        console.log(`Rate limited. Waiting ${waitTime}ms before retry ${attempt + 1}`);
        await sleep(waitTime);
        continue;
      }
 
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
 
      return response;
    } catch (error) {
      lastError = error;
 
      if (attempt < maxRetries - 1) {
        const waitTime = Math.min(1000 * Math.pow(2, attempt), 30000);
        console.log(`Request failed. Retrying in ${waitTime}ms...`);
        await sleep(waitTime);
      }
    }
  }
 
  throw lastError;
}
 
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Implementare Python:

import time
import requests
from functools import wraps
 
def retry_with_backoff(max_retries=5, base_delay=1, max_delay=60):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    response = func(*args, **kwargs)
 
                    if response.status_code == 429:
                        retry_after = response.headers.get('Retry-After')
                        if retry_after:
                            wait_time = int(retry_after)
                        else:
                            wait_time = min(base_delay * (2 ** attempt), max_delay)
 
                        print(f"Rate limited. Waiting {wait_time}s...")
                        time.sleep(wait_time)
                        continue
 
                    response.raise_for_status()
                    return response
 
                except requests.exceptions.RequestException as e:
                    if attempt == max_retries - 1:
                        raise
                    wait_time = min(base_delay * (2 ** attempt), max_delay)
                    print(f"Request failed: {e}. Retrying in {wait_time}s...")
                    time.sleep(wait_time)
 
            raise Exception(f"Max retries ({max_retries}) exceeded")
        return wrapper
    return decorator
 
@retry_with_backoff(max_retries=5)
def call_api(url, headers=None):
    return requests.get(url, headers=headers)

Soluția 2: Coada de Cereri cu Rate Limiter

Implementare token bucket:

class RateLimiter {
  constructor(tokensPerSecond, maxTokens) {
    this.tokensPerSecond = tokensPerSecond;
    this.maxTokens = maxTokens;
    this.tokens = maxTokens;
    this.lastRefill = Date.now();
    this.queue = [];
  }
 
  refillTokens() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.tokensPerSecond);
    this.lastRefill = now;
  }
 
  async acquire() {
    return new Promise((resolve) => {
      const tryAcquire = () => {
        this.refillTokens();
 
        if (this.tokens >= 1) {
          this.tokens -= 1;
          resolve();
        } else {
          // Wait until a token is available
          const waitTime = (1 - this.tokens) / this.tokensPerSecond * 1000;
          setTimeout(tryAcquire, waitTime);
        }
      };
 
      tryAcquire();
    });
  }
 
  async execute(fn) {
    await this.acquire();
    return fn();
  }
}
 
// Usage: 10 requests per second, burst of 20
const limiter = new RateLimiter(10, 20);
 
async function makeRateLimitedRequest(url) {
  return limiter.execute(() => fetch(url));
}
 
// Process multiple requests respecting rate limits
async function processItems(items) {
  const results = [];
  for (const item of items) {
    const result = await makeRateLimitedRequest(`/api/items/${item.id}`);
    results.push(result);
  }
  return results;
}

Python cu biblioteca ratelimit:

from ratelimit import limits, sleep_and_retry
 
# 100 calls per minute
@sleep_and_retry
@limits(calls=100, period=60)
def call_api(url):
    response = requests.get(url)
    if response.status_code == 429:
        raise Exception("Rate limited")
    return response
 
# With custom backoff
import backoff
 
@backoff.on_exception(
    backoff.expo,
    requests.exceptions.RequestException,
    max_tries=5,
    giveup=lambda e: e.response is not None and e.response.status_code < 500
)
def robust_api_call(url):
    response = requests.get(url)
    response.raise_for_status()
    return response

Soluția 3: Cereri în Batch

În loc de apeluri individuale, folosește endpoint-uri batch:

// ❌ Individual requests (high rate limit usage)
for (const userId of userIds) {
  const user = await fetch(`/api/users/${userId}`);
  users.push(user);
}
 
// ✅ Batch request (single rate limit hit)
const users = await fetch('/api/users/batch', {
  method: 'POST',
  body: JSON.stringify({ ids: userIds })
});

GraphQL batching:

// Instead of multiple queries
const queries = userIds.map(id =>
  fetch('/graphql', {
    body: JSON.stringify({ query: `{ user(id: "${id}") { name email } }` })
  })
);
 
// Use batched query
const batchedQuery = `{
  ${userIds.map((id, i) => `user${i}: user(id: "${id}") { name email }`).join('\n')}
}`;
 
const result = await fetch('/graphql', {
  body: JSON.stringify({ query: batchedQuery })
});

Soluția 4: Cache pentru Reducerea Apelurilor

import NodeCache from 'node-cache';
 
const cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
 
async function getCachedData(key, fetchFn) {
  const cached = cache.get(key);
  if (cached) {
    return cached;
  }
 
  const data = await fetchFn();
  cache.set(key, data);
  return data;
}
 
// Usage
async function getUser(userId) {
  return getCachedData(`user:${userId}`, async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  });
}

Cache Redis pentru sisteme distribuite:

import redis
import json
 
redis_client = redis.Redis(host='localhost', port=6379, db=0)
 
def get_with_cache(key, fetch_fn, ttl=300):
    cached = redis_client.get(key)
    if cached:
        return json.loads(cached)
 
    data = fetch_fn()
    redis_client.setex(key, ttl, json.dumps(data))
    return data

Soluția 5: Monitorizare și Alerte

class RateLimitMonitor {
  constructor() {
    this.metrics = {
      totalRequests: 0,
      rateLimited: 0,
      remaining: {},
    };
  }
 
  recordRequest(apiName, response) {
    this.metrics.totalRequests++;
 
    if (response.status === 429) {
      this.metrics.rateLimited++;
      console.error(`RATE LIMITED: ${apiName}`);
      // Send alert to monitoring system
      this.sendAlert(apiName);
    }
 
    const remaining = response.headers.get('X-RateLimit-Remaining');
    const limit = response.headers.get('X-RateLimit-Limit');
 
    if (remaining && limit) {
      this.metrics.remaining[apiName] = {
        remaining: parseInt(remaining),
        limit: parseInt(limit),
        percentage: (parseInt(remaining) / parseInt(limit)) * 100
      };
 
      // Warn at 20% remaining
      if (parseInt(remaining) / parseInt(limit) < 0.2) {
        console.warn(`Low rate limit: ${apiName} has ${remaining}/${limit} remaining`);
      }
    }
  }
 
  sendAlert(apiName) {
    // Integrate with your alerting system
    // e.g., PagerDuty, Slack, email
  }
 
  getMetrics() {
    return this.metrics;
  }
}

Limite de Rată Frecvente pentru API-uri

| API | Limita de Rată | Observații | |-----|------------|-------| | OpenAI | Variază în funcție de tier | Limite pe tokeni + cereri | | GitHub | 5000/oră (autentificat) | 60/oră neautentificat | | Stripe | 100/sec (live), 25/sec (test) | Per cheie API | | Twitter/X | 50-500/15min | Depinde de endpoint | | Google APIs | Variază | Cote per proiect | | Salesforce | 100.000/24h | Depinde de ediție | | Slack | 1/secundă | Metodele Tier 1-4 variază |

Gestionare Specifică pe Tipuri de Erori

async function handleApiResponse(response) {
  switch (response.status) {
    case 429:
      const retryAfter = response.headers.get('Retry-After');
      return {
        error: 'rate_limited',
        retryAfter: retryAfter ? parseInt(retryAfter) : 60,
        strategy: 'exponential_backoff'
      };
 
    case 503:
      // Service unavailable - different from rate limit
      return {
        error: 'service_unavailable',
        retryAfter: 30,
        strategy: 'linear_backoff'
      };
 
    case 401:
      // Auth error - don't retry
      return {
        error: 'unauthorized',
        retryAfter: null,
        strategy: 'fail_fast'
      };
 
    default:
      if (response.ok) {
        return { success: true, data: await response.json() };
      }
      return {
        error: 'unknown',
        status: response.status,
        strategy: 'exponential_backoff'
      };
  }
}

Referință Rapidă: Strategii de Retry

| Scenariu | Strategie | Exemple de Întârzieri | |----------|----------|----------------| | 429 cu Retry-After | Așteaptă timpul exact | Conform specificației | | 429 fără header | Exponential backoff | 1s, 2s, 4s, 8s, 16s | | 500 Server Error | Exponential + jitter | 1-2s, 2-4s, 4-8s | | Timeout rețea | Linear backoff | 5s, 10s, 15s | | Conexiune refuzată | Fail rapid după 3 încercări | 1s, 1s, 1s |

Lista de Bune Practici

  1. Verifică întotdeauna mai întâi headerul Retry-After
  2. Implementează exponential backoff cu jitter
  3. Setează un număr maxim de reîncercări (de obicei 3-5)
  4. Folosește cache pentru răspunsuri când este posibil
  5. Utilizează endpoint-uri batch acolo unde sunt disponibile
  6. Monitorizează proactiv utilizarea rate limit-ului
  7. Distribuie cererile pe ferestre de timp
  8. Ia în considerare chei API multiple pentru volume mari

Integrare API la Volum Mare?

Gestionarea limitelor de rată API la scară necesită o arhitectură atentă. Echipa noastră este specializată în:

  • Implementarea gateway-urilor API
  • Managementul cozilor de cereri
  • Strategii de failover multi-provider
  • Optimizarea costurilor pentru utilizarea API

Solicită expertiză în integrare

Ai nevoie de ajutor cu conformitatea EU AI Act sau securitatea AI?

Programeaza o consultatie gratuita de 30 de minute. Fara obligatii.

Programeaza un Apel

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.