Introduzione
Quando un programma comunica con un’API esterna, può inviare molte richieste in un intervallo di tempo ridotto. Per proteggere le proprie risorse, il server spesso applica un rate limit, cioè un limite al numero di richieste consentite in un determinato periodo. Ad esempio, un’API potrebbe accettare al massimo 60 richieste al minuto per ogni client.
Se il limite viene superato, il server risponde normalmente con lo stato HTTP 429 Too Many Requests. Continuare a inviare richieste senza controllo può peggiorare la situazione, causare blocchi temporanei o portare alla sospensione delle credenziali.
In questo tutorial vedremo come gestire correttamente il rate limiting con la libreria requests. Costruiremo un piccolo client riutilizzabile che:
- controlla il codice di stato HTTP;
- legge l’header
Retry-Afterrestituito dal server; - applica un’attesa tra le richieste;
- limita il numero massimo di tentativi;
- registra informazioni utili tramite il modulo
logging.
Questo argomento è diverso da un semplice timeout o da una strategia generica di retry: qui l’obiettivo principale è rispettare esplicitamente i limiti imposti dal servizio remoto.
Codice completo
import logging
import time
from typing import Any
import requests
# Configurazione di base del logging.
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
class RateLimitedApiClient:
"""Client HTTP minimale con gestione del rate limiting."""
def __init__(
self,
base_url: str,
requests_per_second: float = 2,
max_rate_limit_retries: int = 3,
) -> None:
self.base_url = base_url.rstrip("/")
self.max_rate_limit_retries = max_rate_limit_retries
# Intervallo minimo tra due richieste consecutive.
self.min_interval = 1 / requests_per_second
self.last_request_time = 0.0
self.session = requests.Session()
def _wait_before_request(self) -> None:
"""Attende quanto basta per rispettare il limite locale."""
elapsed = time.monotonic() - self.last_request_time
remaining = self.min_interval - elapsed
if remaining > 0:
time.sleep(remaining)
def _get_retry_after(self, response: requests.Response) -> float:
"""Restituisce i secondi di attesa indicati dal server."""
retry_after = response.headers.get("Retry-After")
if retry_after is None:
# Fallback prudente se l´header non è presente.
return 5.0
try:
# Caso più comune: Retry-After contiene un numero di secondi.
return max(0.0, float(retry_after))
except ValueError:
# Alcune API possono restituire una data HTTP.
# Per semplicità usiamo un´attesa conservativa.
logger.warning(
"Header Retry-After non numerico: %s",
retry_after
)
return 5.0
def get(self, endpoint: str, **kwargs: Any) -> Any:
"""Esegue una richiesta GET rispettando il rate limit."""
url = f"{self.base_url}/{endpoint.lstrip(´/´)}"
rate_limit_attempt = 0
while True:
self._wait_before_request()
self.last_request_time = time.monotonic()
response = self.session.get(url, **kwargs)
if response.status_code != 429:
response.raise_for_status()
return response.json()
rate_limit_attempt += 1
if rate_limit_attempt > self.max_rate_limit_retries:
raise RuntimeError(
"Numero massimo di tentativi superato a causa del rate limit"
)
wait_seconds = self._get_retry_after(response)
logger.warning(
"Rate limit raggiunto. Attesa di %.1f secondi "
"(tentativo %d/%d)",
wait_seconds,
rate_limit_attempt,
self.max_rate_limit_retries
)
time.sleep(wait_seconds)
def close(self) -> None:
"""Chiude la sessione HTTP."""
self.session.close()
if __name__ == "__main__":
client = RateLimitedApiClient(
base_url="https://api.example.com",
requests_per_second=2,
max_rate_limit_retries=3,
)
try:
result = client.get(
"/users",
params={"active": "true"},
timeout=10,
)
print(result)
except requests.RequestException as error:
logger.error("Errore HTTP o di rete: %s", error)
except RuntimeError as error:
logger.error("Errore di rate limiting: %s", error)
finally:
client.close()
Spiegazione
Limitazione locale delle richieste
Il parametro requests_per_second indica quante richieste il client può avviare in un secondo. Con il valore 2, l’intervallo minimo tra due richieste è pari a mezzo secondo.
Il metodo _wait_before_request() calcola il tempo trascorso dall’ultima richiesta usando time.monotonic(). Questa funzione è preferibile a time.time() per misurare intervalli, perché non viene influenzata dai cambiamenti dell’orologio di sistema.
La limitazione locale è utile perché riduce la probabilità di ricevere risposte 429. Tuttavia, non può conoscere sempre il limite reale del server: il servizio potrebbe applicare regole diverse per endpoint, utente, token o indirizzo IP.
Gestione della risposta 429
Quando il server restituisce 429, il codice legge l’header Retry-After. Questo header comunica per quanto tempo il client dovrebbe aspettare prima di riprovare. Un valore come 10 significa attendere dieci secondi.
Se l’header non è presente o non contiene un valore numerico, il client usa un’attesa di sicurezza di cinque secondi. In un progetto reale si può aggiungere il supporto completo al formato data HTTP previsto dallo standard.
Numero massimo di tentativi
Un’API potrebbe continuare a restituire 429 per diversi minuti. Per evitare che il programma rimanga bloccato indefinitamente, il client conta i tentativi e solleva un’eccezione dopo il numero definito da max_rate_limit_retries.
La chiamata raise_for_status() gestisce gli altri errori HTTP, come 401 Unauthorized, 403 Forbidden o 500 Internal Server Error. Questi casi non devono essere trattati automaticamente come rate limiting.
Best practice
- Rispetta la documentazione dell’API: verifica i limiti per minuto, ora o giorno e controlla se esistono header come
X-RateLimit-RemainingeX-RateLimit-Reset. - Non ignorare il codice 429: continuare immediatamente con nuove richieste può aumentare il tempo di blocco.
- Usa un limite locale: una pausa preventiva è spesso più efficiente di numerosi tentativi falliti.
- Imposta sempre un timeout: nell’esempio,
timeout=10impedisce alla richiesta di restare sospesa indefinitamente. - Registra gli eventi importanti: i log aiutano a capire quando il client è stato limitato e quanto ha atteso.
- Evita richieste inutili: usa filtri, seleziona solo i campi necessari e memorizza temporaneamente i dati già recuperati.
- Coordina i client concorrenti: se più thread o processi usano lo stesso token, il limite deve essere condiviso, non gestito separatamente.
- Non ritentare indiscriminatamente: una risposta
400indica spesso una richiesta errata e non migliorerà con un nuovo tentativo identico.
Riepilogo
La gestione del rate limiting è essenziale per costruire client API affidabili. Con requests possiamo controllare la frequenza delle chiamate, riconoscere lo stato 429 Too Many Requests, leggere l’header Retry-After e attendere prima di riprovare.
Una soluzione robusta combina tre livelli: una limitazione locale preventiva, il rispetto delle istruzioni ricevute dal server e un numero massimo di tentativi. In questo modo il programma è più rispettoso dell’API, più prevedibile e meno soggetto a blocchi.
