Défaillances en cascade : quand une erreur d’agent se propage

Les défaillances en cascade apparaissent lorsqu’une erreur d’outil, de service ou d’agent déclenche une chaîne plus large d’échecs.
Sur cette page
  1. Le problème
  2. Pourquoi ça arrive
  3. Pannes les plus fréquentes
  4. Amplification des retries entre couches (Retry amplification)
  5. Saturation du pool partagé (Shared pool saturation)
  6. Dominos de timeout dans services voisins (Timeout domino)
  7. Cascade de coûts sur panne technique (Cost cascade)
  8. Comment détecter ces problèmes
  9. Comment distinguer une panne en cascade d'une erreur locale d'outil
  10. Comment stopper ces pannes
  11. Où c'est implémenté dans l'architecture
  12. Auto-vérification
  13. FAQ
  14. Pages liées

Le problème

La demande paraît standard : construire un profil client et préparer une réponse courte.

Dans les traces, c'est différent : un tool externe a commencé à renvoyer timeout, l'agent est passé en retries, a surchargé le worker pool après 4 minutes, et 7 minutes plus tard, des workflows et services non liés ont commencé à se dégrader.

L'échec initial était local. Mais via la boucle de l'agent, il est devenu systémique.

Le système ne tombe pas immédiatement.

Il entraîne progressivement de plus en plus de dépendances.

Analogie : imagine un embouteillage sur une seule voie d'un pont. Au début, une seule voie ralentit. Puis l'onde d'arrêt atteint toutes les routes d'accès au pont. Une panne en cascade dans un agent fonctionne pareil : un problème local sans limites devient vite un problème partagé du système.

Pourquoi ça arrive

Une défaillance en cascade n'apparaît pas à cause d'une seule réponse "mauvaise" d'outil, mais parce que l'erreur est amplifiée simultanément dans plusieurs couches.

En production, cela ressemble souvent à ceci :

  1. un tool se dégrade (5xx, 429, timeout) ;
  2. des retries démarrent à plusieurs endroits (SDK, gateway, agent) ;
  3. la queue grossit, les workers se bloquent en attente ;
  4. la latency augmente aussi pour d'autres runs, même sans ce tool ;
  5. sans fail-fast et safe-mode, le système continue à multiplier les appels.

Le problème n'est pas seulement un service instable. Runtime n'arrête pas la vague tant qu'elle est encore locale.

Pannes les plus fréquentes

En production, quatre patterns de cascading failures reviennent le plus souvent.

Amplification des retries entre couches (Retry amplification)

Une panne se répète dans le client HTTP, le tool gateway et la boucle de reasoning de l'agent. Le nombre d'appels augmente de façon géométrique. Mini-exemple : 1 failure -> 3 retries dans SDK -> 3 retries dans gateway -> 3 retries dans agent loop = 27 appels.

Cause typique : retry policy dispersée à plusieurs endroits.

Saturation du pool partagé (Shared pool saturation)

Un tool dégradé occupe la majorité des workers. Les autres runs attendent en queue, même si leurs dépendances sont saines.

Cause typique : pas de per-tool bulkhead limits.

Dominos de timeout dans services voisins (Timeout domino)

Quand la queue grossit, le wait time augmente. En conséquence, les services upstream/downstream tombent plus souvent en timeout.

Cause typique : pas de max_seconds strict et pas de fail-fast lors de dégradation de dépendance.

Cascade de coûts sur panne technique (Cost cascade)

La cascade augmente aussi le coût du run : plus de retries, plus de tokens, cycle de run plus long. Même les fins "réussies" deviennent trop coûteuses.

Cause typique : absence d'execution budgets (max_tool_calls, max_retries, max_usd).

Comment détecter ces problèmes

Les pannes en cascade se voient bien via la combinaison des métriques gateway, runtime et queue.

MétriqueSignal de cascading failureAction
retry_amplification_rateune panne crée beaucoup de retries dupliquéscentraliser les retries dans un seul gateway
circuit_open_ratebreaker s'ouvre souvent sur un toolactiver safe-mode et réduire le fan-out
queue_backlogla queue grossit avec un trafic d'entrée normalajouter bulkhead limits et timeout sur le run
cross_service_timeout_ratedes timeouts apparaissent dans des services non liésisoler le tool dégradé et limiter la concurrence
cascading_stop_reason_ratecascade:* stop reasons fréquentesrevoir breaker/bulkhead et stratégie fallback

Comment distinguer une panne en cascade d'une erreur locale d'outil

Un tool_timeout ne signifie pas toujours cascade. Question clé : la panne reste-t-elle locale, ou impacte-t-elle déjà d'autres parties du système.

Normal si :

  • la panne reste isolée dans un seul tool ;
  • queue et latency des autres runs restent stables ;
  • après un court cooldown, le système revient au baseline.

Dangereux si :

  • l'erreur d'un tool augmente globalement queue_backlog ;
  • des timeouts apparaissent dans des workflows non liés ;
  • coût et durée des runs augmentent même là où ce tool n'est pas utilisé.

Comment stopper ces pannes

En pratique :

  1. garder les retries dans un seul choke point (tool gateway) ;
  2. mettre per-tool circuit breaker + cooldown + bulkhead limits ;
  3. définir des execution budgets pour retries, tool calls, temps et coût ;
  4. en dégradation, basculer le run en safe-mode (partial/fallback), pas "forcer".

Guard minimal contre cascade :

PYTHON
from dataclasses import dataclass
import time


RETRYABLE = {408, 429, 500, 502, 503, 504}


@dataclass(frozen=True)
class CascadeLimits:
    max_steps: int = 25
    max_seconds: int = 90
    max_tool_calls: int = 18
    max_retries: int = 4
    max_in_flight_per_tool: int = 8
    open_circuit_after: int = 3
    circuit_cooldown_s: int = 30


class CascadeGuard:
    def __init__(self, limits: CascadeLimits = CascadeLimits()):
        self.limits = limits
        self.steps = 0
        self.tool_calls = 0
        self.retries = 0
        self.in_flight: dict[str, int] = {}
        self.fail_streak: dict[str, int] = {}
        self.circuit_open_until: dict[str, float] = {}
        self.started_at = time.time()

    def on_step(self) -> str | None:
        self.steps += 1
        if self.steps > self.limits.max_steps:
            return "cascade:budget_max_steps"
        if (time.time() - self.started_at) > self.limits.max_seconds:
            return "cascade:budget_timeout"
        return None

    def before_tool_call(self, tool: str) -> str | None:
        if time.time() < self.circuit_open_until.get(tool, 0.0):
            return "cascade:circuit_open"

        current = self.in_flight.get(tool, 0)
        if current >= self.limits.max_in_flight_per_tool:
            return "cascade:bulkhead_full"

        self.tool_calls += 1
        if self.tool_calls > self.limits.max_tool_calls:
            return "cascade:budget_tool_calls"

        self.in_flight[tool] = current + 1
        return None

    def after_tool_call(self, tool: str, status_code: int) -> str | None:
        self.in_flight[tool] = max(0, self.in_flight.get(tool, 1) - 1)

        if status_code in RETRYABLE:
            self.retries += 1
            if self.retries > self.limits.max_retries:
                return "cascade:retry_budget"

            streak = self.fail_streak.get(tool, 0) + 1
            self.fail_streak[tool] = streak
            if streak >= self.limits.open_circuit_after:
                self.circuit_open_until[tool] = time.time() + self.limits.circuit_cooldown_s
                return "cascade:circuit_open"
            return "cascade:retry_allowed"

        self.fail_streak[tool] = 0
        return None

C'est un guard de base. Dans cette version, tool_calls compte les tentatives d'appel, pas seulement les appels admis avec succès. En production, on l'étend généralement avec priorisation de requêtes, limites séparées pour tool critiques et route safe-mode explicite. before_tool_call(...) est appelé avant l'appel externe, et after_tool_call(...) juste après la réponse pour étouffer la cascade le plus tôt possible.

Où c'est implémenté dans l'architecture

En production, le contrôle des cascading failures est généralement réparti sur trois couches système.

Tool Execution Layer est la première barrière : retry policy, circuit breaker, bulkhead, timeout et normalisation d'erreurs. Si cette couche est faible, une panne locale devient vite une vague.

Agent Runtime gère les budgets, stop reasons (cascade:*) et transitions safe-mode. C'est ici qu'il faut stopper un run avant saturation système.

Orchestration Topologies définit comment isoler des branches workflow dégradées, pour qu'un chemin dégradé ne bloque pas tout le workflow.

Auto-vérification

Vérification rapide avant release. Coche les points et regarde le statut ci-dessous.
C'est un sanity-check court, pas un audit formel.

Progression: 0/8

⚠ Il y a des signaux de risque

Il manque des contrôles de base. Fermez les points clés de la checklist avant release.

FAQ

Q : Les retries sont utiles. Pourquoi peuvent-ils casser le système ?
R : Utiles seulement avec backoff, caps et point de contrôle unique. Quand les retries sont dupliqués entre couches, la charge monte plus vite que la récupération.

Q : Pourquoi les systèmes d'agents sont plus sensibles à la cascade que les API classiques ?
R : Parce qu'un agent a une boucle de reasoning et peut répéter le même tool_call plusieurs fois. La panne de dépendance se multiplie à chaque étape du run.

Q : Timeout ne suffit pas ? Pourquoi ajouter breaker et bulkhead ?
R : Timeout limite un appel. Breaker arrête la vague de répétition, et bulkhead empêche un tool de monopoliser tous les workers.

Q : Safe-mode ne dégrade pas la qualité de réponse ?
R : Partiellement oui, mais c'est une dégradation contrôlée. Mieux vaut renvoyer un résultat partiel correct qu'attendre un outage complet.


Une cascading failure ressemble rarement à une seule grosse erreur. Le plus souvent, c'est une chaîne de petites pannes que le système amplifie lui-même. Principe clé : agent loops amplify failures (les agents amplifient les pannes). C'est pourquoi les agents de production ont besoin non seulement de bons modèles, mais aussi de limites strictes au niveau runtime et gateway.

Pages liées

Si ce problème apparaît en production, ces pages sont aussi utiles :

⏱️ 8 min de lectureMis à jour 12 mars 2026Difficulté: ★★☆
Implémenter dans OnceOnly
Guardrails for loops, retries, and spend escalation.
Utiliser dans OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
controls:
  loop_detection:
    enabled: true
    dedupe_by: [tool, args_hash]
  retries:
    max: 2
    backoff_ms: [200, 800]
stop_reasons:
  enabled: true
logging:
  tool_calls: { enabled: true, store_args: false, store_args_hash: true }
Intégré : contrôle en productionOnceOnly
Ajoutez des garde-fous aux agents tool-calling
Livrez ce pattern avec de la gouvernance :
  • Budgets (steps / plafonds de coût)
  • Kill switch & arrêt incident
  • Audit logs & traçabilité
  • Idempotence & déduplication
  • Permissions outils (allowlist / blocklist)
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.
Exemple de policy (concept)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}

Auteur

Nick — ingénieur qui construit une infrastructure pour des agents IA en production.

Focus : patterns d’agents, modes de défaillance, contrôle du runtime et fiabilité des systèmes.

🔗 GitHub: https://github.com/mykolademyanov


Note éditoriale

Cette documentation est assistée par l’IA, avec une responsabilité éditoriale humaine pour l’exactitude, la clarté et la pertinence en production.

Le contenu s’appuie sur des défaillances réelles, des post-mortems et des incidents opérationnels dans des systèmes d’agents IA déployés.