Blog

Retry Storm nelle architetture distribuite: quando la resilienza diventa il problema

Dario Fadda Maggio 26, 2026

Le architetture distribuite moderne sono progettate per la resilienza. Aggiungiamo retry per i fallimenti transitori, replica per la durabilità, autoscaling per l’elasticità, circuit breaker per l’isolamento. Ogni meccanismo, preso singolarmente, migliora la disponibilità. Ma sotto stress, la loro interazione può abbattere l’intero sistema.

La maggior parte delle interruzioni enterprise non è causata dall’assenza di fault tolerance. È causata da meccanismi di fault tolerance non delimitati che reagiscono simultaneamente. Questo articolo analizza il fenomeno del retry storm e mostra come progettare sistemi con bounded reliability.

1. Retry Storm: quando la resilienza moltiplica il traffico

I retry sono progettati per proteggere dai fallimenti temporanei. Ma i retry moltiplicano il carico. Ecco un esempio semplificato della logica di retry che si trova comunemente nei sistemi di integrazione tra servizi:

import time
import random

def downstream_service():
    latency = random.choice([0.1, 0.2, 0.8])
    time.sleep(latency)
    if latency > 0.7:
        raise TimeoutError("Slow response")
    return "OK"

def call_with_retries(max_attempts=3):
    for attempt in range(max_attempts):
        try:
            return downstream_service()
        except TimeoutError:
            print(f"Retry {attempt+1}")
    raise Exception("Failed after retries")

In condizioni normali questa logica funziona correttamente. Ma sotto carico elevato si innesca una spirale:

  1. La latenza aumenta
  2. Scattano i timeout
  3. Ogni richiesta viene ritentata fino a 3 volte
  4. Il traffico verso il backend triplica
  5. Il backend rallenta ulteriormente
  6. Aumentano i retry

In un’architettura a livelli tipica — Gateway → Experience API → Process API → System API → Database — se ogni livello gestisce i retry in modo indipendente, l’amplificazione del carico diventa moltiplicativa, non additiva. Un singolo rallentamento del database può abbattere tre livelli API a cascata in pochi minuti.

Il pattern Bounded Retry (sicuro per la produzione)

La soluzione non è eliminare i retry, ma delimitarli e renderli consapevoli del carico di sistema:

def call_with_bounded_retries(max_attempts=2, system_load=0.5):
    # Fail-fast quando il sistema è sotto stress
    if system_load > 0.75:
        return None

    for attempt in range(max_attempts):
        try:
            return downstream_service()
        except TimeoutError:
            # Exponential backoff con jitter
            backoff = 0.2 * (2 ** attempt)
            time.sleep(backoff + random.uniform(0, 0.1))
    return None

Le differenze chiave rispetto alla versione base:

  • Ceiling sui retry: massimo 2 tentativi invece di 3
  • Exponential backoff: aumenta il tempo di attesa ad ogni tentativo
  • Jitter: aggiunge variabilità casuale per evitare wave di retry sincronizzate
  • Load-aware circuit: disabilita i retry completamente quando il sistema è sovraccarico

2. Fan-out della replica e collasso della coordinazione

La replica sincrona migliora la durabilità dei dati. Ma ogni write si moltiplica per il numero di repliche, aumentando il costo di coordinazione:

def write_to_replicas(data, replicas=3):
    for _ in range(replicas):
        time.sleep(0.2)  # Simula latenza di replica

Sotto traffico elevato, il lag delle repliche cresce, i client iniziano a ritentare le scritture, e il carico effettivo di write raddoppia. In sistemi di elaborazione aziendale (ordini, fatturazione, riconciliazione) questo pattern causa un collasso del throughput non perché i dati vadano persi, ma perché la coordinazione sopraffà il sistema.

La soluzione è la durabilità stratificata: non tutte le scritture richiedono le stesse garanzie. Le transazioni critiche usano replica completa; log ed eventi non critici ne richiedono meno. La reliability deve essere proporzionata, non massimizzata ciecamente.

3. Loop di feedback dell’autoscaling

L’autoscaling reagisce alle metriche di traffico. Ma se queste metriche sono gonfiate dai retry, il sistema escala in risposta a traffico artificiale:

def autoscale_safe(request_rate, sustained_load):
    # Scala su domanda sostenuta, non su spike da retry
    if sustained_load and request_rate > 120:
        print("Scaling up — domanda organica confermata")

Segnali più affidabili su cui basare l’autoscaling:

  • Domanda sostenuta (non spike improvvisi)
  • Tendenze nella distribuzione della latenza (P95, P99)
  • RPS organiche (esclusi i retry)
  • Velocità di crescita delle code

4. Il problema reale: le reazioni correlate

Retry, replica e autoscaling reagiscono ciascuno a segnali diversi. Ma sotto stress, reagiscono tutti allo stesso segnale di degradazione. Questa correlazione crea il fallimento a cascata.

Scenario reale — payment reconciliation service:

  1. La latenza dell’ERP sale a 700ms
  2. Il servizio Billing va in timeout a 500ms
  3. Billing ritenta 3 volte
  4. La Process API ritenta l’orchestrazione
  5. Il Gateway ritenta la richiesta client
  6. L’autoscaling reagisce allo spike
  7. Il lag di replica del database aumenta
  8. La Dead Letter Queue inizia a riempirsi

In pochi minuti, un rallentamento minore diventa un’interruzione di piattaforma. La causa principale: reazione non delimitata.

5. Pattern di Bounded Reliability per sistemi API

Retry Budget

Il carico effettivo è: Carico Effettivo = RPS in ingresso × Numero Retry. Con 1.000 RPS e 3 retry, il backend riceve 3.000 richieste. Impostare un budget massimo di retry per richiesta e per servizio è non negoziabile in produzione.

Classificazione degli Errori

Non tutti gli errori sono retriable. Una tassonomia chiara:

Tipo di Errore Retry? Azione
CONNECTIVITY Bounded retry
TIMEOUT Backoff esponenziale
VALIDATION (4xx) No Fail fast
AUTH (401/403) No Alert immediato

I retry ciechi su errori di validazione o autenticazione sono debito architetturale.

Idempotency obbligatoria

I retry senza idempotency causano corruzione dei dati. Il transaction ID deve essere deterministic, non generato casualmente ad ogni tentativo:

# ❌ Non sicuro: genera un nuovo ID ad ogni retry
transaction_id = uuid()

# ✅ Sicuro: riusa l'ID dalla richiesta originale
transaction_id = payload.get("transaction_id") or request.headers["correlation-id"]

Dead Letter Queue con Osservabilità

Tracciare le seguenti metriche come segnali di early warning:

  • Percentuale di retry sul totale delle richieste
  • Frequenza dei timeout per endpoint
  • Velocità di crescita della DLQ
  • Shift nella distribuzione P95 della latenza

Conclusione

Le retry storm non iniziano con un fallimento catastrofico. Iniziano con un piccolo aumento di latenza, qualche timeout, una manciata di retry. Poi i meccanismi di fault tolerance reagiscono insieme — e la loro interazione non controllata trasforma un disagio minore in un’interruzione totale.

La resilienza nelle architetture distribuite non significa aggiungere più safety net. Significa controllare come quei safety net si comportano sotto stress. Delimita i retry. Classifica i fallimenti. Forza l’idempotency. Scala su domanda organica. Monitora i loop di feedback prima che si avvitino.

La differenza tra una piattaforma resiliente e un fallimento a cascata sta quasi sempre nella risposta a una sola domanda: i tuoi meccanismi di reliability sono controllati o sono illimitati?

Fonte: How Retry Storms Crash API-Led Systems: Bounded Reliability Patterns — DZone

💬 Unisciti alla discussione!


Questo è un blog del Fediverso: puoi trovare quindi questo articolo ovunque con @blog@spcnet.it e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su Retry Storm nelle architetture distribuite: quando la resilienza diventa il problema, utilizza la discussione sul Forum.
Condividi la tua esperienza, confrontati con altri professionisti e approfondisci i dettagli tecnici nel nostro 👉 forum community