Claude API en production : erreurs, retries, backoff et résilience

Cinquième article de la série « Production Engineering, Evals & Security avec Claude ».

Une application Claude qui fonctionne parfaitement en développement peut tomber dès les premiers pics de trafic si aucun chemin d’erreur n’a été prévu.

Le document Production Engineering, Evals & Security insiste sur une idée simple : la première décision à prendre lorsqu’une erreur survient est de savoir si elle est retriable ou terminal. Toute la stratégie de résilience découle de cette classification.


La question fondamentale : attendre puis réessayer peut-il fonctionner ?

Pour classer une erreur, il faut se poser une seule question :

Si j’attends puis je renvoie exactement la même requête, a-t-elle une chance raisonnable de réussir ?

Si oui :

retriable

Si non :

terminal

Exemples :

429 rate limit
→ retriable

529 overloaded
→ retriable

500 server error
→ retriable

400 bad request
→ terminal

401 authentication failure
→ terminal

403 permission failure
→ terminal

Le document donne notamment cette classification :

RETRIABLE = {
    429,
    529,
    500,
    502,
    503,
    504
}

TERMINAL = {
    400,
    401,
    403,
    404
}

Pourquoi cette distinction est essentielle

Une erreur 429 provient d’une limite temporaire.

Le temps peut donc résoudre le problème.

À l’inverse, une erreur 400 signifie que la requête elle-même est incorrecte.

Attendre :

1 seconde
10 secondes
1 minute

ne change rien.

La même requête échouera encore.

C’est pourquoi :

retriable
→ retry

terminal
→ fail fast

Le piège des retries immédiats

Le mauvais pattern classique est :

def call_with_retry(make_call, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            return make_call()
        except Exception:
            time.sleep(0)

    raise RetryBudgetExhausted()

Le défaut est évident :

time.sleep(0)

Le système recommence immédiatement.

Sur un 429, cela donne :

request
  ↓
429
  ↓
retry immédiatement
  ↓
429
  ↓
retry immédiatement
  ↓
429

Chaque retry peut aggraver la situation en consommant encore plus de capacité.

Le document décrit précisément ce cas de production : un développeur avait ajouté des retries instantanés après son premier rate limit, ce qui avait rendu le comportement encore pire.


Exponential backoff

Une stratégie correcte attend de plus en plus longtemps entre les tentatives.

Par exemple :

attempt 1
→ wait 1 s

attempt 2
→ wait 2 s

attempt 3
→ wait 4 s

attempt 4
→ wait 8 s

C’est le principe de :

exponential backoff

On ajoute généralement :

jitter

c’est-à-dire une petite composante aléatoire afin d’éviter que de nombreux clients réessaient exactement au même instant.


Une implémentation plus robuste

Exemple conceptuel fidèle au principe du document :

import random
import time

RETRIABLE = {
    429,
    500,
    502,
    503,
    504,
    529
}

TERMINAL = {
    400,
    401,
    403,
    404
}

def call_with_retry(make_call, max_attempts=5):

    for attempt in range(max_attempts):
        try:
            return make_call()

        except ApiError as exc:
            status = exc.status_code

            if status not in RETRIABLE:
                raise

            if attempt == max_attempts - 1:
                raise RetryBudgetExhausted() from exc

            wait = 2 ** attempt
            jitter = random.uniform(0, 0.5)

            time.sleep(wait + jitter)

L’idée importante est :

retry only retriable errors
+
backoff
+
jitter
+
cap attempts

Le retry budget

Les retries ne doivent jamais être illimités.

Sinon une dépendance défaillante peut bloquer une requête pendant très longtemps et multiplier les coûts.

Il faut donc définir un :

retry budget

Par exemple :

maximum attempts = 3

ou :

maximum total retry time = X

Le document insiste sur le fait que chaque retry inutile consomme :

  • du temps ;
  • des ressources ;
  • du budget ;
  • et réduit la marge disponible pour d’autres erreurs réellement temporaires.

retry-after : écouter le serveur

Le document souligne également l’importance du header :

retry-after

Lorsqu’un 429 ou 529 fournit cette information, le service indique combien de temps attendre avant de recommencer.

Le comportement recommandé est :

retry-after présent
→ utiliser cette valeur

sinon
→ exponential backoff

Conceptuellement :

retry_after = response.headers.get("retry-after")

if retry_after:
    wait = float(retry_after)
else:
    wait = exponential_backoff(attempt)

Le header doit être considéré comme plus précis qu’un délai choisi arbitrairement par l’application.


Les erreurs 5xx

Le document classe les erreurs serveur dans la catégorie retriable :

500
502
503
504

Pourquoi ?

Parce qu’elles correspondent généralement à un problème temporaire côté service.

Par exemple :

500
→ internal server error

503
→ service unavailable

504
→ timeout / gateway timeout

Une nouvelle tentative après attente peut donc fonctionner.


Les erreurs terminales

Certaines erreurs doivent au contraire provoquer un arrêt immédiat.

400 Bad Request

La requête est mal formée.

same request
→ same failure

Il faut corriger la requête.


401 Authentication

La clé ou l’authentification est incorrecte.

Un retry identique ne change rien.


403 Forbidden

La requête n’a pas les permissions nécessaires.

Attendre ne donne pas davantage de droits.


404 Not Found

Dans la classification présentée par le document, cette erreur est traitée comme terminale : la ressource ou le modèle demandé n’est pas trouvé.


Que faire lorsqu’on hésite ?

Le document adopte une position prudente :

En cas de doute, mieux vaut traiter l’erreur comme terminale et la remonter.

Pourquoi ?

Parce qu’une erreur incorrectement classée terminale :

fails loudly

et sera corrigée.

Une erreur incorrectement classée retriable peut :

hammer the service
+
hide the real problem
+
waste retry budget

C’est un principe de sécurité opérationnelle important.


Les timeouts

Les timeouts sont un cas intéressant.

Le document indique qu’ils sont généralement retriable, car une opération peut simplement avoir dépassé le temps d’attente du client.

Mais :

repeated timeout

sur la même requête peut indiquer que le problème n’est plus réellement transitoire.

Il peut alors être nécessaire de modifier :

  • la requête ;
  • la quantité de données ;
  • l’architecture ;
  • la stratégie de timeout.

Autrement dit :

un timeout
→ retry plausible

timeouts répétés
→ investigate root cause

Attention : le SDK Anthropic peut déjà effectuer des retries

C’est un piège de production important.

Le document précise que les clients SDK Anthropic peuvent automatiquement retenter certaines erreurs transitoires avec des délais progressifs, selon leur configuration.

Cela signifie qu’il faut éviter :

SDK retries
   +
application retries

sans coordination.

Supposons :

SDK = 3 attempts
application = 5 attempts

On pourrait potentiellement multiplier le nombre d’appels effectifs.

Le système que l’on pensait limiter à cinq essais peut en déclencher beaucoup plus.


Où doit vivre la politique de retry ?

Deux approches sont possibles.

Option A : laisser le SDK gérer les erreurs transitoires

Puis réserver le code applicatif aux fallbacks spécifiques.

API call
 ↓
SDK retry policy
 ↓
application fallback

Option B : réduire les retries du SDK

Et gérer toute la politique explicitement dans l’application.

API call
 ↓
application retry policy
 ↓
fallback

L’anti-pattern est :

retry loop
inside another retry loop

sans coordination.


Une erreur de tool n’est pas une erreur API

Les agents introduisent une seconde catégorie de panne :

tool execution failure

Par exemple :

Claude
  ↓
tool_use
  ↓
database_search
  ↓
database unavailable

Que doit faire l’application ?

Surtout pas retourner :

""

ou :

null

comme si le tool avait simplement trouvé zéro résultat.


Pourquoi cacher une erreur de tool est dangereux

Supposons :

Tool failed
     ↓
application returns empty result
     ↓
Claude receives "no data"
     ↓
Claude continues reasoning

Le modèle peut alors produire une réponse confiante sur une fausse prémisse.

Le document insiste donc sur le fait que le résultat doit indiquer explicitement l’erreur avec :

is_error: true

Exemple de tool_result en erreur

Conceptuellement :

def run_tool(tool_use):
    try:
        result = execute(tool_use)

        return {
            "type": "tool_result",
            "tool_use_id": tool_use.id,
            "content": result
        }

    except Exception as exc:
        return {
            "type": "tool_result",
            "tool_use_id": tool_use.id,
            "is_error": True,
            "content": f"Tool failed: {exc}"
        }

Ainsi :

Claude
  ↓
tool_use
  ↓
application executes
  ↓
tool fails
  ↓
tool_result + is_error=true
  ↓
Claude knows it failed

Pourquoi renvoyer l’erreur à Claude ?

Parce que le modèle peut alors décider :

try another tool

ou :

ask user for clarification

ou :

stop

L’application ne doit pas transformer une défaillance technique en donnée valide.


Important pour l’examen : Claude demande, l’application exécute

Il faut conserver le modèle mental du tool use :

Application
   ↓
Claude
   ↓
tool_use
   ↓
Application decides / executes
   ↓
tool_result
   ↓
Claude
   ↓
Final response

Claude ne réalise pas lui-même l’action.

Il demande l’exécution du tool.

L’application :

  • valide les paramètres ;
  • vérifie les permissions ;
  • exécute ;
  • capture l’erreur ;
  • renvoie tool_result.

Cette séparation est particulièrement importante pour la fiabilité et la sécurité.


Le cas des refus

Le document attire aussi l’attention sur un comportement différent :

HTTP 200
+
stop_reason = "refusal"

Ce n’est pas une erreur transitoire.

Le transport HTTP a fonctionné.

Le modèle a pris une décision de contenu.

Le document recommande donc de traiter ce cas comme fail fast et de ne pas tenter automatiquement la même requête.

Conceptuellement :

if response.stop_reason == "refusal":
    raise ValueError(
        "Model refused the request."
    )

Le point essentiel :

refusal
≠
429
≠
timeout

Il ne faut pas appliquer la politique de retry des erreurs réseau.


Le tableau de décision

Le document fournit une table très utile pour la production.

ErreurStratégie
429Retry + backoff + retry-after + limite
529Retry avec backoff
5xx transitoireRetry
400Fail fast
401Fail fast
403Fail fast
Tool errorDépend de la cause, toujours la rendre visible
RefusalFail fast, loguer, ne pas retry aveuglément

Les fallbacks

Un retry budget finit toujours par s’épuiser.

Il faut alors décider avant la production ce que l’application fait.

Exemples possibles cités ou suggérés par le document :

cached result
simpler path
fallback path
clean error to user

Par exemple :

API failure
   ↓
3 retries exhausted
   ↓
cached response available?
   ├── yes → use cache
   └── no → graceful error

Le comportement final ne doit pas être :

unhandled exception

par défaut.


Exemple complet de stratégie

def answer_with_resilience(make_call):
    max_attempts = 4

    for attempt in range(max_attempts):
        try:
            return make_call()

        except ApiError as exc:
            status = exc.status_code

            if status in {400, 401, 403, 404}:
                raise

            if status not in {429, 500, 502, 503, 504, 529}:
                raise

            if attempt == max_attempts - 1:
                break

            retry_after = getattr(
                exc,
                "retry_after",
                None
            )

            if retry_after is not None:
                wait = retry_after
            else:
                wait = min(
                    2 ** attempt,
                    8
                )

            wait += random.uniform(0, 0.5)

            time.sleep(wait)

    cached = load_cached_result()

    if cached is not None:
        return cached

    raise ServiceUnavailable(
        "Request could not be completed."
    )

Ce code est illustratif ; le document source définit les principes de classification, backoff, limites et fallback, mais ne prescrit pas cette implémentation exacte.


Pourquoi prévoir le fallback dans le design document ?

Le module précédent de cette série montrait que le design document doit définir :

Failure handling

avant l’implémentation.

Il faut écrire :

429
→ retry

400
→ fail fast

tool unavailable
→ return error to Claude

retry budget exhausted
→ fallback / clean error

Ainsi, la première erreur réelle en production n’est pas le moment où l’équipe découvre qu’aucune décision n’avait été prise.


Tester les failure paths

Les chemins d’erreur doivent eux aussi être testés.

Par exemple :

def test_400_fails_fast():
    ...

def test_429_retries():
    ...

def test_retry_budget_is_capped():
    ...

def test_tool_error_sets_is_error():
    ...

def test_refusal_is_not_retried():
    ...

Une application avec :

excellent happy path
+
untested failure path

n’est pas réellement robuste.


Exemple : test d’un 429

def test_rate_limit_retries():
    calls = 0

    def make_call():
        nonlocal calls
        calls += 1

        if calls < 3:
            raise ApiError(status_code=429)

        return "ok"

    result = call_with_retry(make_call)

    assert result == "ok"
    assert calls == 3

On vérifie ici que le système :

does retry

mais également qu’il :

eventually stops retrying

Exemple : erreur terminale

def test_bad_request_does_not_retry():
    calls = 0

    def make_call():
        nonlocal calls
        calls += 1
        raise ApiError(status_code=400)

    with pytest.raises(ApiError):
        call_with_retry(make_call)

    assert calls == 1

Le comportement attendu est :

400
→ 1 call
→ fail

et non :

400
→ retry x5

Une erreur peut aussi venir du streaming

Le document aborde également le streaming dans la partie production.

Lorsqu’un stream est interrompu en cours de génération, le contenu partiel ne doit pas nécessairement être considéré comme une réponse complète.

Pour le tool use, c’est encore plus critique : les arguments JSON d’un tool peuvent arriver progressivement et ne sont pas sûrs à exécuter tant que le bloc n’est pas complet.

Il faut donc distinguer :

partial stream

de :

completed message

Un stream cassé peut nécessiter de retenter la requête complète, plutôt que de transmettre un résultat incomplet à l’étape suivante.


Anti-patterns à éviter

1. Retry sur toutes les exceptions

except Exception:
    retry()

Mauvais, car :

400
401
403

ne doivent pas être retry.


2. Retry immédiat

time.sleep(0)

Mauvais pour les rate limits.


3. Retry illimité

while True:

sans cap.

Mauvais pour :

latency
cost
service load

4. Cacher un tool failure

except:
    return ""

Très dangereux.


5. Double retry SDK + application

Peut multiplier les appels sans que l’équipe s’en rende compte.


6. Retenter un refusal

Un refusal n’est pas un incident réseau temporaire.


Ce qu’il faut retenir pour la certification

Première question

Would waiting and retrying
the exact same request
plausibly work?

Oui

retriable

Non

terminal

Classification importante

429 → retry
529 → retry
5xx transient → retry

400 → fail fast
401 → fail fast
403 → fail fast
404 → fail fast

selon le tableau présenté dans le module.


Pour une erreur retriable

backoff
+
jitter
+
retry-after
+
attempt cap

Pour une erreur terminale

fail fast

Tool failure

tool_result
+
is_error = true

et ne jamais convertir silencieusement l’erreur en résultat vide.


Refusal

HTTP 200
+
stop_reason = refusal

ne doit pas être traité comme une erreur réseau transitoire.


SDK retries

Toujours vérifier :

what the SDK already retries

avant d’ajouter une couche applicative supplémentaire.


Pièges d’examen

Scénario : l’API retourne 429.

→ Retry avec backoff, idéalement en respectant retry-after, avec nombre de tentatives limité.


Scénario : l’API retourne 400.

Fail fast.


Scénario : l’API retourne 401.

→ Corriger l’authentification ; le retry identique est inutile.


Scénario : un tool échoue et l’application renvoie une chaîne vide à Claude.

→ Mauvaise conception. Renvoyer un tool_result avec is_error: true.


Scénario : le SDK retente déjà trois fois et votre wrapper applicatif retente cinq fois.

→ Risque de retries imbriqués et multiplication involontaire des tentatives.


Scénario : la réponse HTTP est 200, mais stop_reason == "refusal".

→ Ce n’est pas une erreur transitoire. Ne pas appliquer aveuglément la politique de retry réseau.


À retenir en une phrase

La résilience commence par classer correctement les erreurs : retry uniquement ce que le temps peut réparer, avec backoff et budget limité ; faire échouer rapidement les erreurs terminales et toujours rendre les tool failures explicitement visibles à Claude.

Le phare info – Média indépendant & critique
Sélectionne, organise, contextualise et partage des contenus pertinents autour d’un thème ou d’une problématique, dans une logique de veille, de transmission et de mise en sens.
Pour cet article, l’intelligence artificielle a été utilisée comme un outil d’aide à l’exploration, à la structuration et à la rédaction. Elle permet de confronter plusieurs angles, de repérer certains biais humains possibles et de faire émerger des points de vigilance. Le curateur humain observe aussi les biais possibles de l’IA, vérifie les éléments essentiels, nuance l’analyse, corrige les formulations fragiles et assume la publication.

Articles liés

Fiche de révision certification — Accelerators & IP Contribution

Cette fiche résume les notions essentielles du module Accelerators & IP Contribution. Le fil conducteur est simple : Un build Claude n’est pas terminé lorsqu’il fonctionne....

Étude de cas : corriger un accelerator Claude mal packagé, mal versionné et mal sécurisé

Un système Claude peut fonctionner parfaitement en démonstration et pourtant être impropre à la production. Le cas cumulatif du module illustre précisément cette situation. L’application : s’exécute...

Applications Claude multi-composants : trust boundaries, least privilege et sécurité

Une application Claude moderne n’est souvent pas constituée d’un seul appel API. Elle peut combiner : une API applicative ; Claude ; un agent ; Claude Code ; un MCP...

Comparer les plateformes Claude : latency, compliance, data residency et total cost

Lorsque plusieurs plateformes permettent d’exécuter Claude, comment choisir ? Une comparaison superficielle pourrait se limiter à : la plateforme la moins chère ; celle que l’équipe connaît...

Model versioning avec Claude : pin what ships, eval gates et rollback

Changer de modèle dans une application Claude peut sembler être une modification minime : model = "new-model" Pourtant, en production, ce changement peut modifier : la qualité...

Le sentier du savoir

De la curiosité à la transmission, explorez les étapes qui permettent de transformer l’information en compréhension durable.

Étape 1 — Construire une culture générale solide

Construire une base solide de connaissances pour comprendre le monde. Relier les faits, les disciplines et les repères essentiels.

Étape 2 — Maîtriser la pensée critique et l’analyse : apprendre à penser contre ses propres certitudes

Apprendre à analyser l’information, repérer les biais et questionner les évidences. Penser par soi-même dans un monde saturé de récits.

Étape 3 – Apprendre à argumenter et à convaincre

Structurer sa pensée pour convaincre sans manipuler. Savoir débattre, nuancer et formuler des idées claires.

Étape 4 – Approfondir un ou plusieurs domaines d’expertise

Explorer un ou plusieurs domaines en profondeur. Passer de la curiosité à la compréhension experte.

Etapes 5 : Devenir polyglotte : élargir sa pensée par les langues

Élargir ses horizons par le langage et les cultures. Penser autrement en changeant de langue.

Étape 6 — Comprendre la méthode scientifique et expérimenter

Comprendre la méthode scientifique et l’expérimentation. Distinguer savoirs établis, hypothèses et croyances.

Étape 7 – Écrire, transTransmission : écrire, transmettre, enseigner

Écrire, expliquer, partager ce que l’on a compris. Transformer le savoir en outil collectif.

Étape 8 — Cultiver l’équilibre corps-esprit pour soutenir l’érudition

Cultiver le corps et l’esprit pour soutenir l’érudition dans le temps. Le savoir durable repose aussi sur l’attention et l’équilibre personnel.