api

Rate Limiting: ce este, algoritmi principali și capcane frecvente

Rate limiting controlează câte cereri acceptă un API pe unitate de timp. Token bucket, sliding window, 429, Retry-After și capcanele de implementare.

Cuprins

Rate limiting-ul este mecanismul prin care un server limitează numărul de cereri pe care un client le poate trimite într-un interval de timp. Când limita este depășită, serverul respinge cererile suplimentare cu statusul HTTP 429 Too Many Requests, până când fereastra de timp se resetează. Fără rate limiting, un singur client neglijent sau rău intenționat poate consuma toată capacitatea unui API și lăsa restul utilizatorilor fără răspuns.

Nu este doar o problemă de securitate. Rate limiting-ul protejează și baza de date din spatele API-ului, stabilizează latența pentru toți utilizatorii și face previzibilă capacitatea sistemului. Un API fără rate limiting se comportă bine numai când nu este solicitat.

Ce este rate limiting-ul mai exact?

Un contract explicit între server și client: „poți face cel mult N cereri în intervalul T". Când contractul este respectat, serverul procesează normal. Când este încălcat, serverul returnează 429 și, ideal, header-ul Retry-After cu numărul de secunde sau timestamp-ul exact de la care clientul poate reîncerca. Trei headere suplimentare fac contractul transparent:

  • X-RateLimit-Limit: câte cereri sunt permise în fereastra curentă.
  • X-RateLimit-Remaining: câte mai are clientul la dispoziție înainte de blocare.
  • X-RateLimit-Reset: timestamp Unix sau număr de secunde până la resetarea ferestrei.

Aceste headere nu sunt impuse de un standard formal, dar sunt adoptate practic uniform. Un API care returnează 429 fără Retry-After forțează clientul să ghicească când poate reîncerca, ceea ce duce la retry storms: toți clienții blocați reîncearcă simultan după un interval arbitrar și generează un al doilea val de cereri tocmai când fereastra se resetează.

Cum funcționează algoritmii principali de rate limiting?

Există patru algoritmi uzuali, fiecare cu trade-off-uri diferite între memorie, precizie și toleranță la trafic concentrat:

  • Fixed window. Contorul se resetează la intervale fixe, de exemplu la fiecare minut exact. Simplu de implementat cu un singur contor în Redis sau Postgres. Dezavantaj: un client poate trimite N cereri la finalul minutului curent și alte N la începutul minutului următor, adică 2N în mai puțin de două secunde, fără să fie blocat.
  • Sliding window log. Serverul stochează timestamp-ul fiecărei cereri și verifică câte cereri au avut loc în ultimele T secunde reale. Precis, fără efectul de margine al fixed window. Dezavantaj: fiecare cerere înseamnă un element stocat; la volum mare, memoria crește liniar cu traficul.
  • Token bucket. Un „coș" se umple cu jetoane la o rată constantă (de exemplu, 10 jetoane pe secundă, maximum 100). Fiecare cerere consumă un jeton. Dacă coșul e gol, cererea este respinsă. Permite burst-uri scurte dacă coșul este plin, ceea ce mimează comportamentul uman natural mai bine decât o rată strict uniformă.
  • Leaky bucket. Cererile intră într-o coadă și sunt procesate la o rată fixă, indiferent cât de rapid sosesc. Elimină complet burst-urile: outputul este neted. Util pentru sisteme unde ritmul constant contează mai mult decât latența per cerere, cum ar fi procesarea batch sau streaming.

Token bucket este cel mai folosit pentru API-uri publice. Sliding window log apare când fairness-ul per utilizator contează mai mult decât memoria. Fixed window rămâne în uz pentru că e cel mai simplu de implementat cu un INCR și EXPIRE în Redis.

Care sunt capcanele frecvente în implementarea rate limiting-ului?

Cele mai costisitoare greșeli nu sunt în alegerea algoritmului, ci în detaliile de implementare care par minore la început:

  • Rate limit pe IP în loc de utilizator sau token. IP-ul este o identitate slabă: utilizatorii din spatele unui NAT sau VPN partajează același IP. Limitarea pe IP blochează utilizatori legitimi și lasă un atacator cu botnet neatins. Limitați per utilizator autentificat sau per token JWT ori de câte ori aveți un mecanism de autentificare.
  • Contoare în memorie care se pierd la restart. Un contor în memorie al aplicației dispare la fiecare restart. În primele secunde după repornire, toți utilizatorii par la zero cereri și pot depăși limitele fără să fie blocați. Dacă aplicația face deploy des, contorii trebuie să trăiască în afara procesului: Redis pentru viteză sau Postgres pentru persistență și auditabilitate.
  • Header Retry-After omis. Fără Retry-After, clienții care primesc 429 vor reîncerca după intervale arbitrare. Dacă mulți clienți fac asta simultan, generează un al doilea vârf de cereri exact când fereastra se resetează. Adăugarea headerului este o linie de cod și elimină o întreagă clasă de probleme.
  • Back-pressure ignorat, propagat în baza de date. Un API care acceptă toate cererile fără limită transferă presiunea direct în Postgres: fiecare cerere deschide o tranzacție, iar un val simultan se transformă rapid în connection pool exhausted sau lock contention. Back-pressure-ul ignorat la nivel de API devine incident la nivel de bază de date. Rate limiting-ul este prima linie de apărare a bazei de date, nu un feature opțional.
  • Rate limiting doar în aplicație, nu și la proxy. Nginx poate aplica rate limiting cu limit_req_zone înainte ca cererea să atingă aplicația, economisindu-i resurse pentru cererile pe care le-ar respinge oricum. Fără acest strat, aplicația irosește cicli CPU și memorie înainte să returneze 429.

Cum folosim noi rate limiting-ul la crawlerra?

În produsele pe care le construim, rate limiting-ul apare în două contexte distincte: ca apărare a propriilor API-uri și ca disciplină când consumăm API-uri externe.

Pe partea de server, PromoAzi aplică rate limiting în filtru-ul aplicației folosind Bucket4j (in-memory, fără Redis), cu două praguri per IP: 120 de cereri pe minut și 2000 pe oră pentru endpoint-urile generale, respectiv 30 pe minut și 300 pe oră pentru endpoint-urile mai costisitoare (istoric de preț, agregări multi-retailer)1. Identificarea per IP folosește header-ul CF-Connecting-IP, autoritar în spatele Cloudflare; traficul intern și endpoint-urile de admin sunt excluse. Depășirile sunt vizibile în stack-ul de observabilitate și declanșează un alert pe Discord cu un cooldown de cinci minute per IP, ca să nu inundăm canalul în cazul unui atac coordonat.

Pe partea de client, crawlerra-backend apelează API-ul SmartBill pentru facturare. SmartBill impune un limit strict de aproximativ trei cereri pe secundă și răspunde cu 403 timp de până la zece minute în loc de 429 standardizat, fără headere care să indice intervalul de reîncercare2. SmartBillClient-ul nostru tratează acel 403 ca rate limit, așteaptă 60 de secunde și reîncearcă o singură dată; alte erori sunt propagate imediat, ca să nu mascăm bug-uri reale drept throttling. Aceeași disciplină se aplică la scraping pentru produsele proprii: un IP blocat înseamnă oferte care nu mai apar în săptămâna în care contează. Orchestrarea pipeline-urilor trece prin n8n.

Cum verifici că rate limiting-ul funcționează corect?

Trei verificări simple acoperă majoritatea situațiilor. Prima: trimite N+1 cereri rapid cu curl în buclă și confirmă că a N+1-a primește 429 cu header-ul Retry-After prezent. Dacă răspunsul este tot 200, rate limiting-ul nu funcționează sau pragul este mai mare decât crezi.

A doua: verifică comportamentul la restart. Dacă ai contoare în memorie, repornește procesul aplicației și trimite imediat N cereri. Dacă nu primești 429, contorii s-au pierdut și fereastra este complet deschisă după fiecare repornire. Cu contoare în Postgres sau Redis, valorile supraviețuiesc.

A treia: monitorizează rata de 429-uri în producție. Un spike brusc poate însemna un client cu comportament anormal, un atac în desfășurare sau un bug în propriul client. Fără alertă pe această rată, afli doar când utilizatorii se plâng. O practică solidă de site reliability engineering tratează rata de 429 ca metrică de prim-plan, nu ca un status code ignorat în logs.

  1. PromoAzi folosește Bucket4j (in-memory, fără Redis) cu două praguri per IP: general (120/min, 2000/oră) și strict (30/min, 300/oră) pentru endpoint-urile costisitoare. Identificarea trece prin CF-Connecting-IP, iar excesele alertează pe Discord cu cooldown de cinci minute per IP. [promoazi.rate_limit_tiers]
  2. SmartBill răspunde cu 403 (nu 429) când limita de ~3 cereri pe secundă este depășită, blocând până la zece minute. SmartBillClient.java setează DEFAULT_RATE_LIMIT_BACKOFF_MS = 60_000L și reîncearcă o singură dată. [crawlerra.smartbill_backoff]

Întrebări frecvente

Ce cod HTTP returnează serverul când depășești limita de cereri?

HTTP 429 Too Many Requests, însoțit ideal de header-ul Retry-After cu numărul de secunde sau data exactă la care poți reîncerca. Unele implementări mai vechi returnează 503 Service Unavailable, dar 429 este standardul corect conform RFC 6585. Fără Retry-After, clientul ghicește intervalul de reîncercare și riscă să genereze un al doilea val de cereri exact când fereastra se resetează.

Token bucket sau sliding window, pe care îl aleg?

Token bucket pentru API-uri unde burst-uri scurte sunt acceptabile; sliding window log când fairness-ul per utilizator contează mai mult decât memoria. Token bucket este mai simplu și tolerant la burst; sliding window log este precis dar costisitor la volum mare. Pentru API-uri interne cu trafic moderat, fixed window cu un contor în Redis sau Postgres este suficient și cel mai ușor de întreținut.

Pot face rate limiting fără Redis?

Da, cu contoare în memorie dacă rulezi o singură instanță și accepți că limitele se resetează la restart, sau cu Postgres dacă vrei persistență. De îndată ce ai mai multe replici, contoarele per-instanță nu mai funcționează corect: un client poate trimite N cereri pe fiecare instanță înainte să fie blocat global. Nginx cu limit_req_zone rezolvă problema la nivel de proxy fără modificări în aplicație.

Cum se comportă rate limiting-ul cu mai multe instanțe ale aplicației?

Contoarele în memorie per-instanță eșuează: fiecare instanță are propria fereastră și un client poate trimite N cereri pe instanță fără să fie blocat global. Soluția este un contor centralizat: Redis cu INCR și EXPIRE pentru viteză, sau Postgres pentru durabilitate și auditabilitate. Rate limiting la nivel de nginx reverse proxy rezolvă problema și mai sus, înainte ca cererea să atingă aplicația.

Rate limiting vs throttling, care e diferența?

Rate limiting blochează cererile care depășesc o limită și returnează 429; throttling le încetinește sau le pune în coadă. Un API cu rate limiting respinge imediat cererea N+1. Un API cu throttling poate introduce un delay artificial sau muta cererea într-o coadă de așteptare. Throttling este mai prietenos cu clienții dar mai greu de implementat corect; rate limiting este mai simplu și mai previzibil pentru ambele părți.