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.
Les nouvelles tentatives sont le modèle de résilience le plus courant dans les systèmes distribués, et le plus souvent bâclé. Une boucle entourant un appel HTTP s'apparente à une programmation défensive. Mal faite, elle transforme une panne de 30 secondes en une panne de 30 minutes, car des milliers de clients martèlent un serveur en difficulté au même moment. Bien faites, les nouvelles tentatives absorbent les pannes transitoires si proprement que vos utilisateurs ne les remarquent jamais.
Ce guide couvre la logique de nouvelle tentative dont dépendent les systèmes de production : quels codes de statut relancer, la formule de backoff exponentiel avec jitter complet, les en-têtes Retry-After, les clés d'idempotence, les budgets de nouvelle tentative et les coupe-circuits. Vous verrez également comment prouver que votre client se comporte correctement en simulant des 429 et des 503 avec les serveurs maquette Apidog, car un modèle de nouvelle tentative que vous n'avez jamais testé contre un serveur défaillant est une supposition, pas une conception. Les équipes qui construisent la logique de nouvelle tentative d'API fintech l'apprennent à leurs dépens ; vous n'êtes pas obligé.
Pourquoi les nouvelles tentatives naïves aggravent les pannes
Imaginez un service gérant 1 000 requêtes par seconde. Il a un hoquet pendant cinq secondes. Chaque client réessaye immédiatement, trois fois chacun. Votre demande de 1 000 requêtes/seconde devient 4 000 requêtes/seconde visant un serveur déjà à genoux. Il s'écroule complètement. Maintenant, chaque client réessaye à nouveau.
Cette boucle de rétroaction a un nom : une tempête de nouvelles tentatives (retry storm). La bousculade synchronisée lorsque le serveur revient est le « troupeau tonitruant » (thundering herd). Le livre SRE de Google met en évidence ce modèle dans son chapitre sur la gestion des défaillances en cascade : les nouvelles tentatives sans backoff amplifient la charge précisément lorsque le système peut le moins se le permettre, et peuvent maintenir un service hors service longtemps après que la défaillance initiale ait été corrigée.
Deux défauts de conception sont à l'origine de la plupart des tempêtes de nouvelles tentatives :
- Pas de délai entre les tentatives. Les nouvelles tentatives immédiates multiplient la charge pendant la pire période possible.
- Délais fixes. Si chaque client attend exactement une seconde, ils reviennent tous en même temps. Le serveur reçoit des vagues de trafic synchronisé au lieu d'une montée en charge progressive.
La solution n'est pas de « ne jamais réessayer ». La solution consiste à réessayer de manière sélective, avec des délais aléatoires croissants, et avec un plafond strict sur la charge supplémentaire que vos tentatives ajoutent.
Ces échecs doivent être relancés, jamais ceux-là
Avant tout calcul de backoff, votre client a besoin d'une table de décision. Relancer une requête que le serveur a déjà rejetée comme invalide gaspille de la capacité et pollue les journaux. L'objectif est de relancer une défaillance transitoire.
Relancer ceux-ci :
| Signal | Signification |
|---|---|
| 429 Too Many Requests | Vous avez atteint une limite de débit. Reculez et revenez plus lentement. |
| 502 Bad Gateway | Un saut en amont a renvoyé des données erronées. Souvent transitoire. |
| 503 Service Unavailable | Le serveur est surchargé ou redémarre. |
| 504 Gateway Timeout | Une dépendance en amont était trop lente. |
| Réinitialisations de connexion, échecs DNS, timeouts de socket | La requête n'est peut-être jamais arrivée. |
Un timeout de passerelle 504 mérite une attention particulière : l'origine a peut-être traité votre requête même si la passerelle a abandonné l'attente. Cette distinction est importante lorsque nous abordons l'idempotence.
Ne jamais relancer ceux-ci :
| Signal | Signification |
|---|---|
| 400 Bad Request | Votre charge utile est mal formée. Elle le sera aussi la prochaine fois. |
| 401 Unauthorized | Vos identifiants sont erronés ou expirés. Rafraîchissez le jeton, ne bouclez pas. |
| 403 Forbidden | Vous n'avez pas la permission. Relancer ne l'accordera pas. |
| 422 Unprocessable Entity | La validation a échoué. Corrigez les données, pas le timing. |
La règle : relancez lorsque l'échec concerne l'état du serveur ou le réseau. Échouez rapidement lorsque l'échec concerne votre requête. Un 429 se situe entre les deux ; il peut être relancé, mais c'est aussi un signal que votre taux de requêtes global nécessite d'être amélioré, ce qui est un problème de limitation de débit à résoudre en amont de toute boucle de nouvelle tentative.
La formule du backoff exponentiel, et pourquoi le jitter est important
Le backoff exponentiel signifie que chaque nouvelle tentative attend plus longtemps que la précédente, doublant par défaut :
delay = base * 2^retry_count
Avec une base de 500 ms, cela donne 0,5s, 1s, 2s, 4s, 8s. Ajoutez un plafond (par exemple 30 secondes) pour que les délais ne s'étendent pas sur des minutes :
delay = min(cap, base * 2^retry_count)
Cela résout le problème du « martèlement » mais pas celui de la synchronisation. Si 5 000 clients échouent au même instant, un backoff exponentiel simple fait que les 5 000 reviennent à t=0,5s, puis t=1s, puis t=2s. Toujours des vagues. Toujours un troupeau, mais un troupeau plus poli.
Le jitter brise la synchronisation en randomisant le délai. Le blog d'architecture d'AWS a analysé les chiffres dans son analyse du backoff exponentiel et du jitter, simulant des clients concurrents face à une ressource contestée. Le backoff sans jitter produisait toujours des pics d'appels groupés. Le full jitter, qui choisit un délai aléatoire entre zéro et le plafond exponentiel, a produit à la fois le plus petit nombre total d'appels et des temps d'achèvement proches des plus courts :
delay = random_between(0, min(cap, base * 2^retry_count))
Ce résultat surprend les gens. Randomiser jusqu'à zéro semble négligé par rapport à un calendrier de doublement ordonné. Mais répartir les clients uniformément sur la fenêtre est précisément ce qui maintient la charge du serveur stable. L'analyse d'AWS a également testé le « equal jitter » (moitié fixe, moitié aléatoire) et le « decorrelated jitter » ; le full jitter et le decorrelated jitter se sont avérés supérieurs, et le full jitter est le plus simple à écrire correctement. Utilisez-le comme votre modèle de nouvelle tentative par défaut, sauf si des mesures indiquent le contraire.
Respectez Retry-After lorsque le serveur vous le demande
Le backoff est votre client qui devine combien de temps attendre. Parfois, le serveur élimine les suppositions. L' en-tête Retry-After, défini pour les réponses 429 et 503, contient soit un nombre de secondes, soit une date HTTP :
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Lorsque cet en-tête est présent, il annule votre backoff calculé. Le serveur sait quand sa fenêtre de limite de débit se réinitialise ou sa maintenance se termine ; votre calendrier exponentiel, lui, ne le sait pas. Les clients qui ignorent Retry-After sont l'une des raisons pour lesquelles les fournisseurs passent de la limitation de débit à des interdictions pures et simples. Analysez-le, respectez-le, et appliquez toujours votre plafond et votre nombre maximal de tentatives afin qu'un Retry-After: 86400 hostile ou bogué ne puisse pas bloquer votre travailleur pendant une journée.
Idempotence : la précondition pour relancer une requête POST
Voici le piège dans ce 504 mentionné plus tôt. GET, PUT et DELETE sont idempotents par contrat : les envoyer deux fois laisse le système dans le même état. POST ne l'est pas. Si POST /v1/payments expire après que le serveur l'ait traité, votre nouvelle tentative crée un deuxième paiement. Félicitations, vous avez construit une machine à double facturation avec une excellente disponibilité.
La solution est une clé d'idempotence : un identifiant unique généré par le client (généralement un UUID) envoyé comme en-tête sur chaque opération logique. Le serveur stocke la clé avec la première réponse et rejoue cette réponse stockée pour tout duplicata. Les requêtes idempotentes de Stripe fonctionnent exactement de cette manière, et la plupart des API de paiement et de provisionnement ont suivi le mouvement.
Deux règles permettent aux clés de fonctionner :
- Même opération, même clé. Chaque nouvelle tentative d'un paiement logique réutilise une seule clé. Une nouvelle action utilisateur obtient une nouvelle clé.
- Générez la clé avant le premier envoi, pas à l'intérieur de la boucle de nouvelle tentative. Sinon, chaque nouvelle tentative apparaît comme une nouvelle opération et la protection s'évapore.
Si l'API que vous appelez ne prend pas en charge les clés d'idempotence, ne réessayez pas automatiquement les écritures non-idempotentes. Faites remonter l'échec et laissez un humain ou une tâche de rapprochement décider.
Budgets de nouvelle tentative et coupe-circuits : la porte de secours
Le backoff détermine quand les nouvelles tentatives ont lieu. Il ne limite pas leur nombre. Lors d'une longue panne, même les clients bien jitterés accumulent une charge de nouvelles tentatives, et les tentatives en couches se multiplient : si votre passerelle API réessaye 3 fois et votre client de service réessaye 3 fois, un clic d'utilisateur peut devenir 9 requêtes.
Deux mécanismes limitent les dégâts :
Budgets de nouvelle tentative. Au lieu de « 3 nouvelles tentatives par requête », imposez « les nouvelles tentatives peuvent ajouter au maximum 10 % de charge supplémentaire », mesuré sur une fenêtre glissante. Lorsque le budget est épuisé, les échecs sont renvoyés immédiatement. Cela maintient l'amplification des nouvelles tentatives limitée, quel que soit le nombre de requêtes échouant simultanément. Linkerd et Envoy proposent tous deux cette fonctionnalité comme configuration de premier ordre.
Coupe-circuits. Suivez le taux d'échec par dépendance en aval. Lorsqu'il dépasse un seuil, le disjoncteur s'ouvre : les appels échouent instantanément sans toucher le réseau. Après une période de refroidissement, quelques requêtes de sonde vérifient si la dépendance s'est rétablie avant que le disjoncteur ne se referme. Là où le backoff ralentit poliment la bousculade, le disjoncteur l'annule. Toute conception sérieuse de nouvelle tentative associe les deux, car le backoff seul envoie toujours chaque requête à terme.
Un exemple prêt pour la production en Python
Voici le modèle complet en un seul endroit : filtrage des statuts relançables, full jitter, prise en charge de Retry-After, une clé d'idempotence et un plafond de tentatives strict.
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)
À noter : la clé est créée une seule fois, en dehors de la boucle. Retry-After prime sur le backoff calculé mais respecte toujours le plafond. Les statuts non relançables lèvent une erreur immédiatement. Si vous êtes du côté JavaScript, la bibliothèque axios-retry vous offre la même structure avec des hooks retryCondition et retryDelay ; la table de décision reste identique.
Comment tester le comportement de nouvelle tentative avant que la production ne le fasse pour vous
La plupart des équipes déploient du code de nouvelle tentative qui n'a jamais exécuté sa branche d'échec. Le chemin nominal a été testé ; le chemin 503 s'exécute pour la première fois lors d'une panne réelle. Vous pouvez faire mieux avec deux fonctionnalités d'Apidog.
- Simulez les échecs avec des serveurs maquette. Le mock intelligent d'Apidog vous permet de définir un point de terminaison comme
/v1/paymentset de scénariser ses réponses. Faites-le renvoyer un 503 pour les deux premiers appels et un 200 pour le troisième, ou renvoyez un 429 avecRetry-After: 5, ou ajoutez un délai de 15 secondes pour déclencher le timeout de votre client. Dirigez votre client vers l'URL maquette et observez la boucle de nouvelle tentative gérer chaque scénario, sans incident de production requis. - Affirmez le comportement du client avec des scénarios de test. Les scénarios de test Apidog enchaînent les requêtes avec des assertions et des vérifications de timing. Construisez un scénario qui se déclenche contre votre mock défaillant et affirme que l'appel finit par réussir, que le temps total écoulé se situe dans l'enveloppe de backoff attendue, et qu'exactement une ressource a été créée (prouvant que votre clé d'idempotence a fait son travail). Intégrez le scénario dans votre CI et votre logique de nouvelle tentative sera exercée à chaque commit au lieu de chaque panne.
C'est la différence entre « nous avons ajouté des tentatives » et « nous avons vérifié que notre client survit à une dépendance soumise à une limite de débit et partiellement défaillante ». Téléchargez Apidog gratuitement et vous pourrez avoir un serveur maquette défaillant fonctionnant contre votre client en une dizaine de minutes.
FAQ
Dois-je relancer une requête 429 ?
Oui, et c'est le seul statut où le serveur vous indique généralement comment faire. Lisez l'en-tête Retry-After et attendez au moins cette durée ; revenez au backoff exponentiel avec jitter si l'en-tête est manquant. Traitez également les 429 répétés comme un signal pour corriger votre taux de requêtes avec une limitation côté client ou une mise en cache, et non comme une opération normale.
Qu'est-ce que le full jitter ?
Le full jitter choisit chaque délai de nouvelle tentative uniformément au hasard entre zéro et le plafond exponentiel : random(0, min(cap, base * 2^n)). Il empêche les vagues de nouvelles tentatives synchronisées de nombreux clients. Dans les simulations d'AWS, il a surpassé le backoff simple et l'equal jitter tant pour le nombre total d'appels effectués que pour le temps d'achèvement, c'est pourquoi il est le comportement par défaut dans les SDK AWS.
Est-il sûr de relancer des requêtes POST ?
Seulement lorsque la requête est idempotente dans la pratique, ce qui pour POST signifie l'envoi d'une clé d'idempotence sur laquelle le serveur effectue une déduplication. Sans cela, une nouvelle tentative après un timeout peut dupliquer un paiement, une commande ou un enregistrement, car le serveur a pu traiter la requête que vous pensiez échouée. Les agents d'IA appelant des API d'écriture rencontrent ce problème constamment ; les modèles de récupération d'erreur d'agent sont les mêmes que ceux abordés ici : écritures à clé, nouvelles tentatives plafonnées et un coupe-circuit.
Combien de fois dois-je réessayer ?
Trois à cinq tentatives gèrent presque toutes les pannes transitoires ; au-delà, les taux de succès stagnent tandis que la charge et la latence continuent d'augmenter. Associez le plafond par requête à un budget de nouvelle tentative global (par exemple, les nouvelles tentatives peuvent ajouter 10 % de trafic supplémentaire) afin qu'une panne totale ne puisse pas multiplier votre charge. Si une dépendance reste hors service après votre dernière tentative, c'est le territoire du coupe-circuit, pas celui de la nouvelle tentative.
