Aller au contenu principal

Backoff exponentiel sur HTTP 429

Comprendre l’erreur 429

L’erreur HTTP 429 signifie que vous avez depasse les limites de debit (rate limits) de l’API. Le serveur xAI refuse temporairement vos requetes pour proteger l’infrastructure. Cette erreur est normale dans un contexte de traitement par lots : elle indique simplement que vous envoyez des requetes trop rapidement.

La reaction appropriee n’est pas d’abandonner, mais d’attendre avant de reessayer. Le backoff exponentiel est la strategie standard pour gerer cette situation.

Principe du backoff exponentiel

Au lieu de reessayer immediatement (ce qui provoquerait un nouveau 429), vous attendez un delai croissant entre chaque tentative :

  • 1er echec : attendre 1 seconde
  • 2e echec : attendre 2 secondes
  • 3e echec : attendre 4 secondes
  • 4e echec : attendre 8 secondes
  • 5e echec : abandonner

Le delai double a chaque tentative, d’ou le terme “exponentiel”. Cela laisse le temps au serveur de retrouver de la capacite.

Implementation 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",
                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 aleatoire) est important. Si plusieurs coroutines recoivent un 429 au meme moment, sans jitter elles retenteraient toutes exactement en meme temps, provoquant un nouveau 429. Le jitter desynchronise les tentatives.

Utilisation avec la bibliotheque tenacity

Pour un usage en production, la bibliotheque tenacity simplifie l’implementation :

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",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

Le decorateur @retry intercepte les RateLimitError, attend un delai exponentiel (entre 1 et 60 secondes), et reessaye jusqu’a 5 fois. Le code de la fonction reste propre et lisible.

Combiner backoff et semaphore

En pratique, vous combinez le backoff avec un semaphore pour un controle 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 semaphore empeche de surcharger l’API, et le backoff gere les erreurs 429 residuelles. Cette double protection est la configuration recommandee 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",
                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 meritent un retry. En revanche, une BadRequestError (prompt invalide) ne sera pas resolue en reessayant.

Points cles a retenir

  • L’erreur 429 est normale et attendue lors du traitement par lots
  • Le backoff exponentiel double le delai entre chaque tentative (1s, 2s, 4s, 8s…)
  • Le jitter desynchronise les retentatives pour eviter les collisions
  • La bibliotheque tenacity simplifie l’implementation en production
  • Combinez toujours semaphore et backoff pour une resilience maximale
  • Ne retentez pas les erreurs de requete invalide (BadRequestError)