La démo a marché au premier appel. La prod, ce sont les 999 999 autres appels — ceux qui tombent sur une limite de débit pendant un pic de trafic, qui attrapent un 503 en amont au milieu d’un déploy, ou qui restent suspendus sur un socket qui ne répond jamais. La différence entre une intégration jouet et une intégration fiable tient presque entièrement à la façon dont tu gères le chemin malheureux : ce que tu retentes, combien de temps tu attends, et quand tu abandonnes. Rate-le et un bref hoquet du fournisseur se mue en panne que tu t’infliges toi-même, à mesure que chacun de tes workers retente à l’unisson parfait et achève le travail que le limiteur de débit avait commencé.
Voici un parcours pratique, de qualité production : classe les erreurs pour ne retenter que ce qui est retentable, recule de façon exponentielle avec du jitter, respecte Retry-After, pose des timeouts raisonnables, rends les retries sûrs avec l’idempotence, et cesse de matraquer une dépendance morte grâce à un disjoncteur. Le code est en Python avec le SDK OpenAI face à https://api.brievio.com/v1, mais les règles sont les mêmes sur n’importe quel client HTTP et n’importe quel langage.
Ne retente que le retentable — et rien d’autre
Le bug de loin le plus courant dans la gestion d’erreurs des API d’IA, c’est de retenter des choses qui ne réussiront jamais. Un 401 (mauvaise clé), un 400 (requête malformée), un 422 (ton prompt est trop long) — tout cela est déterministe. La seconde tentative échoue exactement comme la première, sauf qu’on en est maintenant à cinq tentatives et plusieurs secondes plus tard. Pire, tu as caché un vrai bug derrière une boucle de retry. Le seul 4xx qui vaille la peine d’être retenté, c’est 429 (limite de débit), parce que celui-là, lui, est transitoire.
- Retente :
429, et500 / 502 / 503 / 504— ce sont des conditions transitoires côté serveur ou capacité. - Retente : les erreurs de connexion et les timeouts de lecture (la requête n’a peut-être jamais atteint le modèle, ou le modèle a répondu dans un socket déjà fermé).
- Ne retente jamais : tout autre
4xx—400,401,403,404,422. Fais-les remonter, alerte dessus, corrige l’appelant.
Un bon réflexe : un retry est un pari que la même requête obtiendra une réponse différente. Ce n’est vrai que lorsque l’échec tenait au timing ou à la capacité, pas à la requête elle-même.
Backoff exponentiel avec jitter
Une fois que tu sais qu’un échec est retentable, la question est de savoir combien de temps attendre. Retenter immédiatement ne sert à rien — la condition qui a causé le 429 est toujours là une milliseconde plus tard. La réponse standard, c’est le backoff exponentiel : attends environ 0.5s, puis 1s, 2s, 4s, chaque tentative doublant, plafonné à un seuil pour qu’une longue reprise ne te bloque pas indéfiniment.
Mais le backoff exponentiel pur a un mode de défaillance redoutable à l’échelle. Si 500 workers se font tous limiter au même instant — ce qui est exactement ce qui arrive pendant un pic — ils reculent tous de la même durée et retentent au même instant, recréant le pic sur une horloge de 2 secondes. C’est le troupeau qui charge (thundering herd). Le remède, c’est le jitter : randomise chaque délai pour que les retries s’étalent. Le full jitter (dormir une durée aléatoire entre zéro et le seuil de backoff) désynchronise le troupeau bien mieux que d’ajouter un petit coup de pouce aléatoire à un délai fixe.
# Un wrapper de retry que tu peux vraiment mettre en prod. Deux règles font l'essentiel :
# 1. Ne retente que ce qui est retentable (429 + 5xx + erreurs de connexion). Jamais 400/401/422.
# 2. Recule de façon exponentielle ET ajoute du jitter, sinon tous les clients retentent au même rythme.
import random
import time
from openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutError
client = OpenAI(
api_key="sk-brievio-...",
base_url="https://api.brievio.com/v1",
timeout=30, # plafond strict par requête — voir « Timeouts » plus bas
)
RETRYABLE_STATUS = {429, 500, 502, 503, 504}
MAX_ATTEMPTS = 5
BASE_DELAY = 0.5 # secondes
MAX_DELAY = 20.0 # plafonne le backoff pour qu'une reprise lente ne bloque pas indéfiniment
def chat_with_retry(**kwargs):
for attempt in range(MAX_ATTEMPTS):
try:
return client.chat.completions.create(**kwargs)
except APIStatusError as e:
# Un 4xx qui n'est pas un 429, c'est TON bug (mauvais params, mauvaise clé, trop long).
# Le retenter ne fait que brûler de la latence — échoue vite.
if e.status_code not in RETRYABLE_STATUS:
raise
last_error = e
except (APIConnectionError, APITimeoutError) as e:
# Coupure réseau ou notre timeout a sauté. On peut retenter une lecture sans risque.
last_error = e
if attempt == MAX_ATTEMPTS - 1:
break
# Backoff exponentiel avec FULL jitter : sleep ∈ [0, base * 2**attempt].
# Le full jitter (pas « backoff + petit random ») est ce qui désynchronise vraiment
# un troupeau qui charge. Voir le AWS Architecture Blog sur le backoff avec jitter.
ceiling = min(MAX_DELAY, BASE_DELAY * (2 ** attempt))
time.sleep(random.uniform(0, ceiling))
raise last_errorNote le plafond de tentatives (MAX_ATTEMPTS = 5) et le plafond de délai (MAX_DELAY). Les retries sans bornes, c’est ainsi qu’un hoquet passager devient un arriéré qui ne se vide jamais : les requêtes s’accumulent plus vite qu’elles ne se résolvent, la latence grimpe, et les appelants en amont expirent et retentent leurs requêtes à leur tour. Borne les deux, et laisse l’échec être visible plutôt que tamponné.
Respecte Retry-After — ne devine pas
Ta courbe de backoff est une estimation du moment où la capacité se libère. Quand le serveur te donne la réponse directement, utilise-la. Un 429 transporte souvent un en-tête Retry-After (delta-secondes ou date HTTP) qui reflète la vraie fenêtre de réinitialisation. Dormir pendant cette valeur est strictement meilleur que n’importe quelle formule, parce que c’est une vérité de terrain et non une heuristique.
# Quand le serveur te dit combien de temps attendre, écoute-le. Un 429 (et parfois
# un 503) transporte un en-tête Retry-After. Le respecter vaut mieux que n'importe
# quelle courbe de backoff que tu inventes, car il reflète la vraie fenêtre de
# réinitialisation — pas une estimation.
import email.utils as eut
import time
def retry_delay(resp_headers, attempt, base=0.5, cap=20.0):
# 1. Privilégie l'instruction du serveur.
ra = resp_headers.get("retry-after")
if ra is not None:
try:
return float(ra) # forme delta-secondes : « 2 »
except ValueError:
when = eut.parsedate_to_datetime(ra) # forme date HTTP
return max(0.0, when.timestamp() - time.time())
# 2. Pas d'en-tête ? Reviens au backoff exponentiel avec full jitter.
import random
return random.uniform(0, min(cap, base * (2 ** attempt)))
# Notes :
# - Certains fournisseurs envoient aussi X-RateLimit-Reset ; traite-le de la même façon.
# - Ajoute un petit plancher (p. ex. 50 ms) pour qu'un « Retry-After: 0 » ne parte pas en boucle folle.
# - Brievio expose Retry-After sur les 429 au lieu de faire silencieusement traîner
# le socket, donc ce chemin est atteignable — tu peux vraiment reculer sur signal.Cela ne fonctionne que si la passerelle renvoie réellement l’en-tête au lieu d’absorber la limite et de faire traîner ton socket pendant quatre-vingt-dix secondes. Brievio échoue vite et fort — une limite de débit revient sous la forme d’un 429 propre avec Retry-After, pas d’une connexion suspendue — donc le chemin « écoute le serveur » est atteignable. La taxonomie complète des erreurs, indiquant quels codes transportent quels en-têtes, est dans la référence des erreurs.
Timeouts : le mode de défaillance que personne ne teste
Une politique de retry ne sert à rien si la requête ne revient jamais pour être retentée. Un appel d’IA sans timeout finira, tôt ou tard, par se suspendre — une connexion à moitié ouverte, un amont bloqué, un répartiteur de charge qui a lâché le flux. Sans délai limite, cette unique requête retient un worker, une connexion, et une place dans chaque file derrière elle jusqu’à ce que l’OS abandonne quelques minutes plus tard. Pose un timeout explicite par requête (le timeout=30 ci-dessus) pour qu’un appel bloqué se convertisse en une APITimeoutError que tu peux retenter, plutôt qu’en poids mort.
- Le timeout par requête borne une tentative unique. Pour le streaming, le budget pertinent est le temps jusqu’au premier token plus un timeout d’inactivité entre chunks, pas un total au temps de l’horloge.
- Le délai limite global borne toute la séquence de retry. Suis un budget (p. ex. 60s de bout en bout) et arrête de retenter une fois qu’il est épuisé — un appelant qui t’attend a sa propre échéance.
- Timeout > p99 attendu, pas p50. Pose-le juste au-dessus de ta vraie latence de queue. Trop serré et tu annuleras de bonnes requêtes qui étaient sur le point de réussir, fabriquant de la charge par impatience.
Idempotence : rendre les retries sûrs
Les retries introduisent un danger subtil. Quand une requête expire, tu ne sais pas si le serveur l’a traitée — ta lecture de la réponse a échoué, mais le travail a peut-être abouti. Retente à l’aveugle et tu peux double-facturer un client, envoyer une notification deux fois, ou écrire une ligne en double. Les lectures sont naturellement sûres à retenter. Les effets de bord, non.
La parade, c’est une clé d’idempotence : un identifiant unique que tu attaches à une opération logique pour que le serveur (ou ton propre gestionnaire) fusionne les doublons. Pour de purs appels d’inférence, il n’y a généralement aucun effet de bord externe à craindre — mais dès qu’une complétion déclenche une écriture en base, un paiement, ou un message sortant, génère une clé stable par unité logique de travail et déduplique dessus. La règle d’or : si un retry peut se produire deux fois, conçois comme si ça allait arriver.
Disjoncteurs : arrête de t’acharner sur une dépendance morte
Le backoff gère une seule requête en difficulté. Il ne fait rien pour une panne prolongée — si un amont est complètement à terre pendant deux minutes, chaque requête déroule toute son échelle de retry, attend le maximum, et échoue quand même, pendant que ta latence et ta profondeur de file explosent. Un disjoncteur court-circuite cela : après N échecs consécutifs il « s’ouvre » et fait échouer les nouveaux appels immédiatement (ou les route vers un repli) pendant une fenêtre de refroidissement, puis laisse passer une seule sonde pour tester la reprise avant de se refermer.
- Fermé : fonctionnement normal, les requêtes passent, les échecs sont comptés.
- Ouvert : seuil déclenché — rejette vite pendant une période de refroidissement au lieu d’empiler des retries voués à l’échec.
- Semi-ouvert : après le refroidissement, autorise une requête d’essai ; un succès referme le disjoncteur, un échec le rouvre.
Associe le disjoncteur à un chemin de repli et la panne devient une dégradation au lieu d’un échec brutal. C’est aussi là qu’une passerelle justifie sa présence : Brievio fait du basculement inter-fournisseurs, de sorte qu’un unique fournisseur qui tombe peut router vers un fournisseur sain avant même que ton disjoncteur ait besoin de s’ouvrir. Pour voir comment cela s’inscrit dans un vrai budget de fiabilité, lis comment nous concevons un SLO de 99,95 %.
Un mot sur ce que coûtent les retries
Un réglage agressif des retries rend les gens nerveux à propos de la facture — chaque retry est un appel facturable de plus, non ? Pas sur une passerelle qui facture honnêtement. Brievio ne facture rien sur les appels 4xx/5xx en échec, donc le 429 qui a déclenché ton backoff et le 503 que tu as retenté sont gratuits. Tu paies la tentative qui renvoie réellement une complétion, pas celles que le système a rejetées. Cela veut dire que tu peux régler le nombre de tentatives et les timeouts pour la fiabilité sans pénalité de compteur pour autant — et si des retries qui s’emballent restent une inquiétude budgétaire, plafonne directement les dépenses comme décrit dans comment plafonner tes dépenses d’API d’IA.
À retenir
La gestion d’erreurs en production pour les API d’IA, c’est six règles, et tu peux toutes les mettre en prod en un après-midi :
- Ne retente que
429et5xx. Ne retente jamais d’autres4xx— ce sont tes bugs, mis au jour. - Recule de façon exponentielle avec full jitter, et plafonne à la fois les tentatives et le délai.
- Respecte
Retry-Afterquand le serveur l’envoie — il bat n’importe quelle formule. - Pose des timeouts explicites par requête et un délai limite global ; ne laisse jamais un appel se suspendre.
- Rends les retries à effets de bord sûrs avec une clé d’idempotence.
- Ajoute un disjoncteur pour qu’une panne prolongée se dégrade au lieu de fondre.
Rien de tout cela n’est exotique — c’est l’infrastructure ennuyeuse qui tient l’ennuyeuse promesse de rester debout. Elle fonctionne aussi au mieux sur une couche de base qui échoue vite, fait remonter de vrais signaux, et ne te facture pas les échecs, pour que ton réglage soit honnête et gratuit. Si tu hésites encore sur où diriger ton trafic, le comportement de fiabilité en cas d’échec est l’une des choses à tester en premier — abordé dans comment choisir une passerelle d’API d’IA.