Backoff exponentiel sur HTTP 429
Mis à jour le 30 juillet 2026
Comprendre l’erreur 429
L’erreur HTTP 429 signifie que vous avez dépassé les limites de débit (rate limits) de l’API. Le serveur xAI refuse temporairement vos requêtes pour protéger l’infrastructure. Cette erreur est normale dans un contexte de traitement par lots : elle indique simplement que vous envoyez des requêtes trop rapidement.
La réaction appropriée n’est pas d’abandonner, mais d’attendre avant de réessayer. Le backoff exponentiel est la stratégie standard pour gérer cette situation.
Principe du backoff exponentiel
Au lieu de réessayer immédiatement (ce qui provoquerait un nouveau 429), vous attendez un délai croissant entre chaque tentative :
- 1er échec : attendre 1 seconde
- 2e échec : attendre 2 secondes
- 3e échec : attendre 4 secondes
- 4e échec : attendre 8 secondes
- 5e échec : abandonner
Le délai double à chaque tentative, d’où le terme “exponentiel”. Cela laisse le temps au serveur de retrouver de la capacité.
Implémentation manuelle
import asyncio
import random
from openai import RateLimitError
async def request_with_backoff(
prompt: str,
max_retries: int = 5,
base_delay: float = 1.0
) -> str:
for attempt in range(max_retries):
try:
response = await client.chat.completions.create(
model="grok-4.5",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
except RateLimitError:
if attempt == max_retries - 1:
raise # dernier essai, on propage l'erreur
delay = base_delay * (2 ** attempt)
jitter = random.uniform(0, delay * 0.1)
wait_time = delay + jitter
print(f"Rate limit atteint, tentative {attempt + 1}/{max_retries}. "
f"Attente de {wait_time:.1f}s")
await asyncio.sleep(wait_time)
Le jitter (variation aléatoire) est important. Si plusieurs coroutines reçoivent un 429 au même moment, sans jitter elles retenteraient toutes exactement en même temps, provoquant un nouveau 429. Le jitter désynchronise les tentatives.
Utilisation avec la bibliothèque tenacity
Pour un usage en production, la bibliothèque tenacity simplifie l’implémentation :
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type
)
from openai import RateLimitError
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=1, max=60),
retry=retry_if_exception_type(RateLimitError)
)
async def robust_request(prompt: str) -> str:
response = await client.chat.completions.create(
model="grok-4.5",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
Le décorateur @retry intercepte les RateLimitError, attend un délai exponentiel (entre 1 et 60 secondes), et réessaye jusqu’à 5 fois. Le code de la fonction reste propre et lisible.
Combiner backoff et sémaphore
En pratique, vous combinez le backoff avec un sémaphore pour un contrôle complet :
async def resilient_batch(prompts: list[str], max_concurrent: int = 5) -> list[str]:
sem = Semaphore(max_concurrent)
async def single_request(prompt: str) -> str:
async with sem:
return await request_with_backoff(prompt)
tasks = [single_request(p) for p in prompts]
return await asyncio.gather(*tasks, return_exceptions=True)
Le sémaphore empêche de surcharger l’API, et le backoff gère les erreurs 429 résiduelles. Cette double protection est la configuration recommandée pour la production.
Adapter le backoff au type d’erreur
Toutes les erreurs ne justifient pas un retry. Voici une classification :
from openai import RateLimitError, APITimeoutError, APIConnectionError, BadRequestError
async def smart_retry(prompt: str, max_retries: int = 5) -> str:
for attempt in range(max_retries):
try:
return await client.chat.completions.create(
model="grok-4.5",
messages=[{"role": "user", "content": prompt}]
)
except RateLimitError:
delay = 2 ** attempt
await asyncio.sleep(delay)
except (APITimeoutError, APIConnectionError):
delay = 2 ** attempt
await asyncio.sleep(delay)
except BadRequestError:
raise # erreur dans le prompt, retry inutile
Les erreurs 429 (rate limit), les timeouts et les erreurs de connexion méritent un retry. En revanche, une BadRequestError (prompt invalide) ne sera pas résolue en réessayant.
Points clés à retenir
- L’erreur 429 est normale et attendue lors du traitement par lots
- Le backoff exponentiel double le délai entre chaque tentative (1s, 2s, 4s, 8s…)
- Le jitter désynchronise les nouvelles tentatives pour éviter les collisions
- La bibliothèque
tenacitysimplifie l’implémentation en production - Combinez toujours sémaphore et backoff pour une résilience maximale
- Ne retentez pas les erreurs de requête invalide (
BadRequestError)