DEV Community

Cover image for Logique de réessai API et Backoff Exponentiel : Des modèles qui ont fait leurs preuves
Antoine Laurent
Antoine Laurent

Posted on Originally published at apidog.com

Logique de réessai API et Backoff Exponentiel : Des modèles qui ont fait leurs preuves

Nouvelles tentatives d'API : backoff, jitter et idempotence en production

Votre appel d'API de paiement a échoué à 2 heures du matin. S'agissait-il d'un problème réseau temporaire, d'une limite de débit ou d'un serveur hors service ? La réponse détermine si une nouvelle tentative sauve la transaction ou débite deux fois un client.

Essayez Apidog dès aujourd'hui

Les nouvelles tentatives sont un mécanisme de résilience courant dans les systèmes distribués, mais elles sont souvent mal implémentées. Une boucle autour d'un appel HTTP peut absorber une panne transitoire ou, au contraire, transformer une interruption de 30 secondes en panne de 30 minutes en surchargeant un serveur déjà défaillant.

Ce guide présente une stratégie complète :

  • les codes de statut à relancer ;
  • le backoff exponentiel avec full jitter ;
  • l'en-tête Retry-After ;
  • les clés d'idempotence ;
  • les budgets de nouvelle tentative ;
  • les coupe-circuits ;
  • les tests avec les serveurs maquette Apidog.

Les équipes qui implémentent une logique de nouvelle tentative d'API fintech l'apprennent souvent à leurs dépens. Vous pouvez éviter ces erreurs.

Pourquoi les nouvelles tentatives naïves aggravent les pannes

Imaginez un service qui traite 1 000 requêtes par seconde et subit une panne de cinq secondes. Si chaque client réessaie immédiatement trois fois, le trafic passe temporairement à 4 000 requêtes par seconde. Le serveur, déjà en difficulté, s'effondre davantage et déclenche une nouvelle vague de tentatives.

Cette boucle de rétroaction est une tempête de nouvelles tentatives (retry storm). Lorsque le serveur revient, les clients synchronisés créent un troupeau tonitruant (thundering herd).

Comme l'explique le chapitre de Google SRE sur la gestion des défaillances en cascade, les nouvelles tentatives sans délai amplifient la charge au pire moment.

Les deux défauts les plus fréquents sont :

  • aucun délai entre les tentatives ;
  • un délai fixe : si tous les clients attendent exactement une seconde, ils reviennent simultanément.

La solution n'est pas de ne jamais réessayer, mais de le faire :

  1. de manière sélective ;
  2. avec des délais croissants et aléatoires ;
  3. avec un plafond strict sur la charge supplémentaire.

Quels échecs relancer ?

Avant de calculer le backoff, définissez une table de décision. Relancer une requête rejetée parce qu'elle est invalide gaspille des ressources et pollue les journaux.

Relancer les erreurs transitoires

Signal Signification
429 Too Many Requests Une limite de débit est atteinte. Ralentissez avant de revenir.
502 Bad Gateway Un serveur en amont a renvoyé une réponse invalide, souvent temporairement.
503 Service Unavailable Le serveur est surchargé ou redémarre.
504 Gateway Timeout Une dépendance en amont a répondu trop lentement.
Réinitialisation de connexion, échec DNS, timeout de socket La requête n'est peut-être jamais arrivée au serveur.

Un 504 Gateway Timeout est particulier : l'origine peut avoir traité la requête même si la passerelle a cessé d'attendre. Cette distinction devient essentielle pour les opérations non idempotentes.

Ne pas relancer les erreurs permanentes

Signal Signification
400 Bad Request La charge utile est mal formée.
401 Unauthorized Les identifiants sont invalides ou expirés. Rafraîchissez le jeton.
403 Forbidden Les permissions sont insuffisantes.
422 Unprocessable Entity La validation a échoué. Corrigez les données.

Règle pratique : relancez les erreurs liées au réseau ou à l'état du serveur. Échouez rapidement lorsque le problème vient de la requête.

Le statut 429 est intermédiaire : il peut être relancé, mais il signale aussi que votre taux de requêtes global doit être corrigé avec une limitation côté client, une mise en cache ou une meilleure stratégie de consommation d'API. Voir le guide sur la limitation de débit.

Backoff exponentiel et full jitter

Le backoff exponentiel augmente le délai après chaque échec :

delay = base * 2^retry_count
Enter fullscreen mode Exit fullscreen mode

Avec une base de 500 ms, les délais sont de 0,5 s, 1 s, 2 s, 4 s et 8 s. Ajoutez un plafond pour éviter des attentes interminables :

delay = min(cap, base * 2^retry_count)
Enter fullscreen mode Exit fullscreen mode

Le backoff seul évite le martèlement continu, mais pas la synchronisation. Si 5 000 clients échouent au même instant, ils peuvent toujours revenir à 0,5 s, puis 1 s, puis 2 s.

Le jitter randomise le délai. Le full jitter choisit une durée aléatoire entre zéro et le plafond exponentiel :

delay = random_between(0, min(cap, base * 2^retry_count))
Enter fullscreen mode Exit fullscreen mode

L'analyse AWS du backoff exponentiel et du jitter montre que cette stratégie réduit les pics d'appels groupés, le nombre total de requêtes et le temps d'achèvement dans les scénarios de concurrence.

AWS a également comparé le equal jitter et le decorrelated jitter. Le full jitter est généralement le meilleur choix par défaut : il est simple et efficace.

Respecter Retry-After

Le backoff est une estimation côté client. L'en-tête Retry-After permet au serveur d'indiquer directement quand réessayer :

HTTP/1.1 429 Too Many Requests
Retry-After: 12
Enter fullscreen mode Exit fullscreen mode

Cet en-tête peut contenir un nombre de secondes ou une date HTTP. Lorsqu'il est présent, il doit remplacer le délai calculé, tout en respectant votre plafond et votre nombre maximal de tentatives.

Le serveur connaît la durée de sa fenêtre de limitation ou de sa maintenance ; votre client ne la connaît pas. Ignorez donc Retry-After le moins possible. Analysez-le, rejetez les valeurs invalides et plafonnez toujours le résultat afin qu'un Retry-After: 86400 ne bloque pas un worker pendant une journée.

Consultez la définition de l'en-tête dans la documentation MDN : Retry-After.

Idempotence : condition préalable pour relancer un POST

Les méthodes GET, PUT et DELETE sont idempotentes par contrat : les exécuter deux fois doit laisser la ressource dans le même état. POST ne l'est pas nécessairement.

Si POST /v1/payments expire après avoir été traité par le serveur, une nouvelle tentative peut créer un second paiement. Une bonne disponibilité ne doit jamais se transformer en double facturation.

La solution est une clé d'idempotence : générez un identifiant unique, généralement un UUID, et envoyez-le dans chaque tentative d'une même opération. Le serveur associe cette clé à la première réponse et renvoie cette réponse pour les duplicatas.

Les requêtes idempotentes de Stripe suivent ce modèle, adopté par la plupart des API de paiement et de provisionnement. Pour approfondir, consultez ce guide sur les clés d'idempotence.

Deux règles sont essentielles :

  • Une opération logique, une clé. Toutes les tentatives d'un même paiement réutilisent la même clé.
  • Générez la clé avant la boucle. Si vous la recréez à chaque tentative, chaque requête ressemble à une nouvelle opération.

Si l'API ne prend pas en charge l'idempotence, ne relancez pas automatiquement les écritures non idempotentes. Remontez l'échec et laissez un humain ou une tâche de rapprochement décider.

Budgets de tentative et coupe-circuits

Le backoff indique quand réessayer, mais ne limite pas l'impact global. Les tentatives imbriquées peuvent aussi multiplier les appels : une passerelle qui réessaie trois fois et un client qui réessaie trois fois peuvent transformer un clic en neuf requêtes.

Budget de nouvelle tentative

Au lieu de limiter chaque requête à trois tentatives, imposez un budget global, par exemple :

Les nouvelles tentatives ne doivent pas ajouter plus de 10 % de trafic sur une fenêtre glissante.

Lorsque le budget est épuisé, renvoyez immédiatement les échecs. Cette approche limite l'amplification, même lorsque de nombreuses requêtes échouent simultanément. Linkerd et Envoy proposent cette fonctionnalité comme configuration de premier ordre.

Coupe-circuit

Suivez le taux d'échec de chaque dépendance. Au-delà d'un seuil, le coupe-circuit s'ouvre et les appels échouent immédiatement, sans solliciter le réseau.

Après une période de refroidissement, envoyez quelques requêtes de sonde. Si la dépendance répond correctement, refermez progressivement le circuit.

Le backoff ralentit la charge ; le coupe-circuit l'interrompt. Une stratégie de production sérieuse utilise les deux.

Exemple Python prêt pour la production

Cet exemple combine :

  • le filtrage des statuts relançables ;
  • le full jitter ;
  • Retry-After ;
  • une clé d'idempotence ;
  • un plafond strict de tentatives.
import random
import time
import uuid
import requests

RETRYABLE = {429, 502, 503, 504}
BASE = 0.5     # seconds
CAP = 30.0     # ceiling on any single delay
MAX_RETRIES = 5

def create_payment(payload):
    idempotency_key = str(uuid.uuid4())  # one key per logical payment
    headers = {"Idempotency-Key": idempotency_key}

    for retry_count in range(MAX_RETRIES + 1):
        try:
            resp = requests.post(
                "https://api.acmepay.com/v1/payments",
                json=payload, headers=headers, timeout=10,
            )
            if resp.status_code < 400:
                return resp.json()
            if resp.status_code not in RETRYABLE:
                resp.raise_for_status()  # 400/401/403/422: fail fast
            retry_after = resp.headers.get("Retry-After")
        except (requests.ConnectionError, requests.Timeout):
            retry_after = None  # network fault: fall through to backoff

        if retry_count == MAX_RETRIES:
            raise RuntimeError("payment failed after all retries")

        if retry_after and retry_after.isdigit():
            delay = min(CAP, float(retry_after))
        else:
            delay = random.uniform(0, min(CAP, BASE * 2 ** retry_count))
        time.sleep(delay)
Enter fullscreen mode Exit fullscreen mode

La clé est créée une seule fois, en dehors de la boucle. Retry-After prime sur le backoff calculé, mais le délai reste plafonné. Les statuts non relançables échouent immédiatement.

En JavaScript, axios-retry fournit une structure similaire avec retryCondition et retryDelay. La table de décision reste la même.

Tester les échecs avant la production

La plupart des équipes testent le chemin nominal, puis exécutent la première réponse 503 lors d'une panne réelle. Testez plutôt les scénarios d'échec avec Apidog :

  1. Simulez les réponses avec un serveur maquette. Configurez /v1/payments pour renvoyer 503 sur les deux premiers appels, puis 200 sur le troisième. Testez également un 429 avec Retry-After: 5 et un délai de 15 secondes pour déclencher un timeout.
  2. Vérifiez le comportement avec des assertions. Confirmez que l'appel finit par réussir, que le temps total respecte l'enveloppe de backoff et qu'une seule ressource est créée. Ce dernier contrôle vérifie votre clé d'idempotence.
  3. Intégrez le scénario à la CI. La branche d'échec sera ainsi testée à chaque commit plutôt qu'à chaque incident.

Avec les serveurs maquette Apidog, vous pouvez tester un client soumis à une limitation de débit ou à une dépendance partiellement indisponible sans provoquer d'incident de production. Téléchargez Apidog gratuitement pour commencer.

FAQ

Dois-je relancer une requête 429 ?

Oui, mais lisez d'abord Retry-After et attendez au moins la durée indiquée. Si l'en-tête est absent, utilisez un backoff exponentiel avec jitter.

Des 429 répétés indiquent toutefois un problème de débit à corriger avec de la limitation côté client, de la mise en cache ou une réduction du trafic inutile.

Qu'est-ce que le full jitter ?

Le full jitter choisit aléatoirement chaque délai entre zéro et le plafond exponentiel :

random(0, min(cap, base * 2^n))
Enter fullscreen mode Exit fullscreen mode

Il empêche les vagues synchronisées de clients. Les simulations AWS montrent qu'il réduit le nombre total d'appels et le temps d'achèvement par rapport au backoff simple et à l'equal jitter.

Est-il sûr de relancer des requêtes POST ?

Uniquement si le serveur déduplique les requêtes grâce à une clé d'idempotence. Sans cette protection, un timeout peut dupliquer un paiement, une commande ou un enregistrement déjà traité.

Les modèles de récupération d'erreur pour les agents d'IA reposent sur les mêmes principes : écritures idempotentes, tentatives plafonnées et coupe-circuit.

Combien de fois dois-je réessayer ?

Trois à cinq tentatives suffisent généralement pour les pannes transitoires. Au-delà, le taux de succès progresse peu tandis que la charge et la latence augmentent.

Combinez un plafond par requête avec un budget global — par exemple, 10 % de trafic supplémentaire maximum — afin qu'une panne complète ne multiplie pas votre charge. Si la dépendance reste indisponible après la dernière tentative, laissez le coupe-circuit prendre le relais.

Top comments (0)