Aller au contenu principal

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 tenacity simplifie 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)