ELAI S.r.l.

Riprovare aiuta il servizio AI o lo sovraccarica? Il costo nascosto dei tentativi

Probabilità, richieste annidate, jitter e budget: tre esperimenti riproducibili per capire quando riprovare amplifica un guasto.

Riprovare aiuta il servizio AI o lo sovraccarica? Il costo nascosto dei tentativi

Una seconda possibilità consuma risorse reali

Un’applicazione aziendale chiede a un modello AI di analizzare un documento. La risposta tarda, il client scade e riprova. Nel frattempo anche il servizio intermedio e la libreria che chiama il modello hanno una politica di ripetizione. Quante elaborazioni arrivano davvero al componente più costoso? E attendere un intervallo casuale risolve il problema? Seguiremo la stessa richiesta attraverso tre esperimenti piccoli e verificabili, distinguendo affidabilità teorica, moltiplicazione delle chiamate e distribuzione temporale. Il lettore arriverà a riconoscere quali garanzie dà ciascun meccanismo e quali richiedono misure sul servizio reale.

Abstract. Con fallimenti indipendenti di probabilità q e al massimo N tentativi totali, ricaviamo il costo atteso e la probabilità di successo. Poi abbandoniamo l’indipendenza: tre livelli che esauriscono tre tentativi ciascuno possono generare 27 chiamate finali per una richiesta originaria. Un budget di 20 ripetizioni per 100 richieste iniziali limita il costo a 120 tentativi nel modello senza ricarica. Infine 100 client, con seed dichiarato, distribuiscono una singola ondata di retry in 100 millisecondi: il picco su intervalli da 10 millisecondi scende da 100 a 14, ma il numero totale resta 100. Sono conteggi didattici eseguiti, non benchmark di un server o di un prodotto EL-AI.

Contar antes de calcular: intento no significa repetición

Un tentativo è una chiamata effettiva. Una ripetizione, o retry, è un tentativo successivo al primo. Diremo sempre N=3 tentativi totali: uno iniziale e al massimo due retry. La richiesta originaria rappresenta invece l’obiettivo dell’utente, che può generare molte chiamate. Questa distinzione impedisce errori di un fattore significativo quando si moltiplicano limiti fra livelli. Non assumiamo che il provider fatturi tutte le chiamate allo stesso modo: qui il costo è un numero di tentativi, non euro, token elaborati o secondi di GPU.

La guida tecnica di Marc Brooker per AWS, pubblicata nel 2019, distingue timeout, attese crescenti, casualizzazione e limiti alle ripetizioni; descrive anche l’amplificazione fra livelli. È esperienza operativa documentata, non un esperimento che abbiamo riprodotto su AWS. Qui isoliamo matematicamente alcuni effetti con ipotesi nostre, senza trasferire risultati o impostazioni di un SDK a tutti i servizi AI. Il comportamento concreto dipende da semantica degli errori, cancellazione e risorse del sistema.

Il caso favorevole: fallimenti indipendenti

Immaginiamo prima che ogni chiamata fallisca con probabilità q, sempre uguale e indipendente dalle altre. Un successo interrompe le ripetizioni. Per arrivare al secondo tentativo deve fallire il primo: probabilità q. Per arrivare al terzo devono fallire entrambi: q². Il numero atteso è la somma delle probabilità di raggiungere ciascun tentativo, perché ogni tentativo aggiunge una unità al conteggio soltanto quando viene effettivamente raggiunto. Questa è la domanda risolta dalla prima formula.

E[A] = Σ(i=0…N−1) q^i = (1−q^N)/(1−q), for q ≠ 1 P(success within N attempts) = 1−q^N q=1: E[A]=N, P(success)=0

A è il conteggio senza unità, N il massimo totale e q un numero fra 0 e 1. Con q=0,5 e N=3 otteniamo 1+0,5+0,25=1,75 tentativi attesi e successo 0,875. Non vuol dire che una richiesta faccia 1,75 chiamate: è una media teorica su ripetizioni dello stesso processo. Con q=0,9 lo stesso limite produce 2,71 tentativi attesi e successo 0,271. Aumentare il limite quando il servizio fallisce quasi sempre compra poco successo e molto carico. Se q dipende dal carico creato dai retry, queste formule non descrivono più il sistema; non le useremo per prevedere un outage reale.

qNTentativi attesiProbabilità successo
0.131.110.999
0.531.750.875
0.932.710.271

Tre livelli: quando tre diventa ventisette

Ora consideriamo un caso deterministico: tutte le chiamate finali falliscono, ogni livello riconosce il fallimento solo dopo che quello sottostante ha esaurito i suoi tentativi e nessuno cancella il lavoro. Il livello esterno prova tre volte; per ciascuna prova il livello intermedio ne fa tre; per ciascuna di queste il livello finale ne fa altre tre. Il conteggio finale è 3 × 3 × 3=27. La funzione ricorsiva allegata enumera l’albero: non campiona guasti e non usa la probabilità q del modello precedente.

Leaf calls in the declared all-fail tree: C(D,N)=N^D N=3; D=1,2,3,4 → C=3,9,27,81

D conta i livelli che ripetono, non tutti i passaggi software. Se soltanto un livello effettua retry e gli altri propagano l’esito, il conteggio del medesimo modello diventa 3. Non è una dimostrazione che ripetere sempre in alto sia ottimo: ripetere un flusso intero può rifare elaborazioni già riuscite, mentre un retry locale può recuperare soltanto un passaggio economico. Il risultato dimostra perché la politica debba considerare l’intero percorso. Se un componente continua a lavorare dopo il timeout del chiamante, possono esistere anche chiamate sovrapposte, che il nostro albero sequenziale non misura.

Un budget limita il numero, non promette il successo

Poniamo ora 100 richieste iniziali, al massimo 3 tentativi ciascuna e un contatore condiviso di 20 token per le sole ripetizioni. Non ci sono ricariche nella finestra studiata; ogni retry deve consumare atomicamente un token. I primi tentativi non consumano questo contatore. Il massimo aggregato è 100+min(20,100 × 2)=120, contro 300 senza budget nel caso di fallimento totale. Il codice esegue un’allocazione semplice: tutte le 100 chiamate iniziali falliscono, poi 20 richieste ottengono una seconda chiamata che fallisce anch’essa. Le altre 80 non ottengono il primo retry; nessuna ottiene un terzo tentativo.

Il limite 120 è una proprietà di ammissione, non una misura di capacità del server. Tutte le richieste possono comunque fallire. Un contatore locale per ciascuno di dieci processi con 20 token non equivale a un budget globale 20: può ammettere 200 ripetizioni. Un sistema reale deve definire ambito, ricarica, concorrenza e comportamento quando il contatore non è disponibile. Il nostro modello senza ricarica non è una simulazione completa di token bucket. Inoltre l’ordine scelto non assicura equità fra clienti: un limite di carico e una politica di priorità rispondono a domande differenti.

Jitter: distribuire una folla senza ridurla

Nel terzo esperimento 100 client devono effettuare esattamente un retry ciascuno. Con attesa fissa 100 millisecondi arrivano tutti insieme. Con full jitter ciascuno estrae uniformemente un ritardo fra 0 e 100 millisecondi. Usiamo random.Random di Python 3.14.0 con seed 20261017 e salviamo ogni ritardo. Dividiamo il tempo in intervalli da 10 millisecondi: il conteggio casualizzato è 9,7,9,12,14,14,7,11,6,11. Il massimo è 14, mentre la politica fissa concentra 100 arrivi nell’intervallo che inizia a 100 millisecondi. In entrambi i casi sommiamo 100 retry.

B_k = min(B_max, B₀ · 2^k) Fixed wait: W_k=B_k; full jitter: W_k ~ Uniform(0,B_k) E[W_k]=B_k/2 under this uniform model

La formula descrive come si potrebbe estendere l’esperimento a più ondate: k parte da 0 per il primo retry, B₀ è l’attesa iniziale e B_max il tetto, tutti in millisecondi. Nell’esperimento eseguito c’è una sola ondata con B₀=100: non misuriamo il recupero dopo una sequenza esponenziale. La media del ritardo uniforme è 50 millisecondi, non 100: confrontare solo picchi nasconde questa differenza di attesa. Non abbiamo simulato coda, tempo di servizio, errori dipendenti dal carico o latenza del modello. Quattordici arrivi per intervallo non significa quattordici richieste simultaneamente in esecuzione, né un miglioramento garantito del 99 esimo percentile.

Sinistra: chiamate finali nel caso dichiarato di fallimento totale, tre tentativi a ogni livello. Destra: arrivi di una singola ondata di100 retry, per intervalli da10ms, seed20261017; il jitter modifica il picco, non l’area totale. Nessuna misura di server reale.
Sinistra: chiamate finali nel caso dichiarato di fallimento totale, tre tentativi a ogni livello. Destra: arrivi di una singola ondata di100 retry, per intervalli da10ms, seed20261017; il jitter modifica il picco, non l’area totale. Nessuna misura di server reale.

Il tempo massimo appartiene al compito intero

Un limite per chiamata non è il limite del compito. Supponiamo tre tentativi, ciascuno al massimo 200 millisecondi, con attese 50 e 100: il percorso peggiore vale 3 × 200+50+100=750 millisecondi, esclusi altri costi. Se il chiamante concede 600 millisecondi, dopo due tentativi falliti e le due attese sono già trascorsi 550: restano 50, non 200. Il terzo tentativo deve rispettare quel residuo oppure non essere ammesso. Anche questo conto è un limite del modello, non un tempo osservato. Un timer che non include DNS, connessione o lettura del corpo non rappresenta automaticamente il tempo completo.

Per un servizio AI conta anche la cancellazione: il client che smette di attendere non prova che la GPU abbia smesso di generare. Ripetere può quindi duplicare lavoro ancora attivo. Se l’operazione modifica dati o avvia un’azione, il timeout non prova che l’effetto non sia avvenuto. La correttezza richiede una semantica di idempotenza o una riconciliazione dell’esito; questo articolo non implementa tali garanzie. Limitare i retry protegge una risorsa ma non rende sicura una mutazione duplicata. Analogamente, un errore di credenziali o di schema non diventa risolvibile soltanto aspettando.

Un seed non è una garanzia: la probabilità di un picco

Il valore 14 è il risultato della sequenza casuale salvata, non un massimo dimostrato per il metodo. Possiamo però chiedere quanto sia probabile, nel modello ideale, trovare almeno 25 arrivi in uno dei dieci intervalli. Ogni client sceglie un intervallo con probabilità 0,1, indipendentemente dagli altri. Per un singolo intervallo il conteggio X segue una distribuzione binomiale con 100 prove e probabilità 0,1. Non assumiamo invece che i conteggi dei dieci intervalli siano indipendenti: la loro somma è vincolata a 100. Questa differenza è importante per non moltiplicare probabilità senza giustificazione.

E[X]=100·0.1=10; Var(X)=100·0.1·0.9=9 P(X≥25)=Σ(j=25…100) binom(100,j)·0.1^j·0.9^(100−j) P(maximum over10bins≥25) ≤ 10·P(X≥25) ≈ 0.000130728

Il coefficiente binomiale conta in quanti modi j client possono scegliere quell’intervallo; i due fattori successivi esprimono le probabilità di entrare e restarne fuori. Il programma somma tutti i casi da 25 a 100 e ottiene circa 0,0000130728 per un intervallo. La disuguaglianza dell’unione somma i dieci rischi e dà un limite superiore circa 0,000130728 per almeno un picco di 25 o più. È un numero adimensionale, equivalente a circa 0,0131%, e non una probabilità di outage: non sappiamo quanti arrivi il server possa gestire. Il limite vale soltanto con le estrazioni indipendenti uniformi e le finestre fissate qui; client sincronizzati da una stessa sorgente casuale, timer grossolani o retry aggiuntivi cambierebbero il modello.

Abbiamo così tre livelli diversi di evidenza. Il picco 14 è un’osservazione numerica riproducibile della nostra sequenza. La media 10 e il limite probabilistico derivano dalle ipotesi della distribuzione. La capacità di evitare un sovraccarico del servizio rimane invece una domanda empirica, perché richiede tempi e risorse non modellati. Tenere distinti questi piani evita un errore frequente nella divulgazione tecnica: trasformare una simulazione ordinata in una promessa operativa. Anche cambiare la larghezza degli intervalli modifica il picco contato; per confrontare due prove occorre conservare la stessa definizione della metrica.

Che cosa resta da verificare nel servizio reale

I tre esperimenti sono separati deliberatamente: non abbiamo collegato il q indipendente a una coda o tradotto il picco 14 in una percentuale di successo. Per un confronto operativo occorrerebbero distribuzione degli arrivi originali, durata e costo delle chiamate, propagazione della cancellazione, codici di errore e politiche effettive di ogni livello. Un test dovrebbe distinguere richieste utente completate da tentativi, lavoro utile da lavoro duplicato, latenza media da percentili e recupero da sovraccarico. Sono requisiti di un esperimento futuro, non risultati attribuiti a EL-AI o a un provider.

Riprovare aiuta quando esiste una possibilità concreta di recupero e il costo aggiuntivo resta compatibile con risorse e scadenza. Il jitter distribuisce l’arrivo delle ripetizioni, un budget ne limita il numero e una politica coordinata evita la moltiplicazione inconsapevole fra livelli: nessuno dei tre sostituisce gli altri. Il risultato scientifico dell’esempio è questa separazione, sostenuta da conteggi e ipotesi controllabili. Un servizio che riceve meno picchi può continuare a fare troppo lavoro totale; un servizio con budget può comunque non riuscire a completare alcuna richiesta.

Marc Brooker, Timeouts, retries, and backoff with jitter. AWS technical guide, 5 December 2019. Operational source, not a peer-reviewed AI benchmark; read 17 October 2026..

from experiment import expected, nested
print(expected(.5,3), nested(3,3))

Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 17 ottobre 2026.