Retry Policy — politica di ripetizione delle richieste — è un insieme di regole che determinano quando e come un'applicazione mobile ripete automaticamente le chiamate di rete fallite. Con connessioni instabili o errori temporanei del server, una politica di ripetizione ben progettata migliora l'affidabilità dell'applicazione senza intervento dell'utente. Secondo una ricerca di Google Developer Relations (2025), l'implementazione corretta della Retry Policy riduce la percentuale di richieste perse del 40–60% nelle applicazioni mobili con frequenti operazioni di rete.
Punti chiave
Retry Policy — una strategia software che definisce il comportamento del cliente in caso di fallimento di una richiesta di rete: quali errori devono essere ripetuti, quante volte, con quale ritardo e quando interrompere i tentativi. Nelle applicazioni mobili, la politica di ripetizione è di importanza critica a causa dell'instabilità delle reti mobili e dei possibili guasti temporanei del server.
Una Retry Policy di base include tre parametri: numero massimo di tentativi (maxRetries), ritardo iniziale (baseDelay) e la strategia di backoff. Inoltre, può essere specificato un elenco di codici di stato HTTP che devono attivare un tentativo e un timeout per interrompere tutti i tentativi.
Secondo il libro “Designing Data-Intensive Applications” di Martin Kleppmann, il 50% dei guasti nei sistemi distribuiti sono temporanei e vengono risolti con un tentativo. Questo rende la Retry Policy uno dei modi più efficaci ed economici per migliorare la tolleranza ai guasti di un'applicazione mobile senza modificare l'architettura del server.
Gli errori temporanei (ripetibili) sono l'unico tipo di guasto a cui la Retry Policy dovrebbe rispondere. Questi includono timeout di connessione (SocketTimeoutException), indisponibilità temporanea del server (HTTP 503, 502) ed errori DNS. Gli errori permanenti — HTTP 400, 401, 403, 404 — non devono essere ripetuti, poiché indicano un problema nella richiesta, non nella rete o nel server.
Secondo l'AWS Architecture Blog, la classificazione corretta degli errori in ripetibili e non ripetibili è la decisione più importante nella progettazione di una Retry Policy. Ripetere una richiesta non idempotente con HTTP 401 può portare al blocco dell'account, e ripetere HTTP 400 può creare dati duplicati. Configurare sempre esplicitamente l'elenco dei codici da ripetere.
Intervallo fisso — la strategia più semplice: ogni tentativo viene eseguito dopo lo stesso intervallo di tempo. Ad esempio, con un ritardo di 2 secondi, l'applicazione ripete la richiesta dopo 2, 2, 2 secondi. L'intervallo fisso è semplice da implementare e prevedibile, ma crea un carico uniforme sul server in caso di guasti di massa.
Intervallo incrementale — il ritardo aumenta linearmente ad ogni tentativo: primo tentativo dopo 1 secondo, secondo dopo 2, terzo dopo 3 e così via. Questa strategia dà al server più tempo per riprendersi in caso di guasti ripetuti, ma è ancora prevedibile per molti client che falliscono simultaneamente.
| Strategia | Formula del ritardo | Tempo cumulativo (3 tentativi) | Applicazione |
|---|---|---|---|
| Fisso | delay = D | 3 × D | Scenari semplici, timeout locali |
| Incrementale | delay = N × D | 6 × D | Riduzione graduale del carico |
| Esponenziale | delay = D × 2^N | 7 × D | Guasti di massa, servizi cloud |
| Esponenziale + Jitter | delay = random(0, D × 2^N) | variabile | Carico elevato, microservizi |
La scelta della strategia dipende dalla natura dell'applicazione. Per le attività in background di sincronizzazione dei dati sui dispositivi mobili, la strategia esponenziale con jitter è ottimale — offre la più alta probabilità di successo con il minimo carico sul server e sul dispositivo dell'utente.
Exponential backoff — una strategia in cui il ritardo tra i tentativi raddoppia ad ogni tentativo. Se il ritardo iniziale è di 1 secondo, la sequenza dei ritardi sarà 1, 2, 4, 8, 16 secondi. Questo dà al server un tempo di recupero esponenzialmente crescente.
Jitter — una deviazione casuale del ritardo che impedisce richieste di tentativo sincrone da più client (problema del thundering herd). Senza jitter, mille client con la stessa Retry Policy ripeterebbero le richieste contemporaneamente, creando un carico di picco sul server. Lo jitter distribuisce i tentativi nel tempo.
Le coroutine di Kotlin consentono di implementare l'exponential backoff con jitter senza bloccare il thread principale. La funzione retry di kotlinx-coroutines accetta una condizione di ripetizione e un blocco con il corpo della richiesta, gestendo automaticamente ritardi e numero di tentativi.
suspend fun RetryPolicy.executeWithRetry(
block: suspend () -> Result<T>
): Result<T> {
var lastError: Throwable? = null
repeat(maxRetries + 1) { attempt ->
try {
return block()
} catch (e: Exception) {
if (!isRetriable(e) || attempt == maxRetries) {
return Result.failure(e)
}
val delay = (baseDelayMs * (1 shl attempt))
.toLong()
val jitteredDelay = (delay * (0.5 + Random.nextDouble())).toLong()
delay(jitteredDelay)
lastError = e
}
}
return Result.failure(lastError!!)
}
La funzione executeWithRetry accetta una lambda con una chiamata di rete e la esegue con exponential backoff e jitter. Se l'errore non è ripetibile o il numero massimo di tentativi viene superato, la funzione restituisce un errore. Il ritardo viene moltiplicato per un fattore casuale da 0.5 a 1.5 per una distribuzione uniforme dei tentativi.
Circuit Breaker — un pattern di progettazione che impedisce richieste di tentativo infinite durante l'indisponibilità prolungata del servizio. Quando il numero di errori supera una soglia, il Circuit Breaker passa allo stato OPEN e restituisce immediatamente un errore senza eseguire la richiesta, dando al server tempo per riprendersi.
Nelle applicazioni mobili, il Circuit Breaker è particolarmente utile quando un'API non è disponibile a causa di manutenzione programmata o guasti di rete dell'operatore. Senza di esso, l'applicazione consumerebbe batteria e traffico in tentativi infiniti, degradando l'esperienza utente e riducendo l'autonomia del dispositivo.
Il Circuit Breaker ha tre stati: CLOSED (funzionamento normale, le richieste vengono eseguite), OPEN (guasto, le richieste vengono bloccate) e HALF_OPEN (richiesta di prova per verificare il recupero). Dopo un timeout specificato nello stato OPEN, l'interruttore passa a HALF_OPEN ed esegue una richiesta — in caso di successo torna a CLOSED, in caso di fallimento a OPEN.
class CircuitBreaker(
private val failureThreshold: Int = 3,
private val timeoutMs: Long = 30000
) {
private var state = State.CLOSED
private var failureCount = 0
private var lastFailureTime: Long = 0
suspend fun T.protect(block: suspend () -> T): T {
checkState()
return try {
val result = block()
onSuccess()
result
} catch (e: Exception) {
onFailure()
throw e
}
}
}
L'implementazione del Circuit Breaker in Kotlin contiene un contatore di errori e un timer di recupero. Il metodo protect verifica lo stato corrente prima di eseguire la richiesta incapsulata e aggiorna il contatore degli errori in caso di guasto. Dopo aver raggiunto failureThreshold, tutte le richieste vengono immediatamente rifiutate fino alla scadenza di timeoutMs.
Le reti mobili hanno caratteristiche che rendono la Retry Policy particolarmente importante. Il passaggio tra Wi-Fi e dati mobili, la perdita di segnale in metropolitane e tunnel, i blocchi temporanei a livello di operatore — tutti questi scenari portano a guasti delle richieste che possono essere gestiti con successo dai tentativi.
Su Android, la libreria Retrofit e OkHttp forniscono un meccanismo di ripetizione integrato tramite Interceptor. Su iOS, il compito viene risolto tramite URLSessionConfiguration e delega personalizzata. Per lo sviluppo multipiattaforma, Ktor (KMP) include il supporto integrato per la ripetizione con strategie configurabili.
Combine — il framework di Apple per la programmazione reattiva. L'operatore retry in Combine ripete il publisher un numero specificato di volte in caso di errore, ma non consente di configurare il ritardo tra i tentativi. Per una Retry Policy completa, viene utilizzata una combinazione personalizzata di catch e flatMap con ritardo.
extension Publisher {
func retryWithBackoff(
retries: Int = 3,
baseDelay: TimeInterval = 1.0
) -> AnyPublisher<Output, Failure> {
return self.catch { error -> AnyPublisher in
guard retries > 0 else {
return Fail(error).eraseToAnyPublisher()
}
return Just(())
.delay(for: .seconds(baseDelay), scheduler: DispatchQueue.main)
.flatMap { self.retryWithBackoff(
retries: retries - 1,
baseDelay: baseDelay * 2
) }
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
L'estensione retryWithBackoff per Publisher in Combine implementa l'exponential backoff tramite chiamate ricorsive con contatore decrescente e ritardo raddoppiato. L'operatore delay crea una pausa tra i tentativi, mentre catch intercetta l'errore e decide se riprovare o restituire un fallimento.
Il primo errore — ripetere le richieste senza verificare l'idempotenza. Se il server ha creato una risorsa ma non ha restituito conferma a causa di un guasto di rete, un tentativo creerà un duplicato. Per le richieste POST, utilizzare sempre una chiave di idempotenza (Idempotency-Key) nell'intestazione o limitare i tentativi solo a GET, PUT e DELETE.
Il secondo errore — ripetere all'infinito. Impostare sempre un numero massimo di tentativi (3–5 per applicazioni mobili) e un timeout totale per tutti i tentativi. I tentativi infiniti scaricano la batteria e creano un carico parassita sul server, specialmente durante migrazioni di database o modifiche API.
Il terzo errore — ignorare il contesto dell'applicazione. Se l'utente ha chiuso l'applicazione o è passato in background, le Retry Policy attive devono essere annullate correttamente. Utilizzare coroutine con SupervisorScope o Combine con il ciclo di vita dell'interfaccia utente per l'annullamento automatico dei tentativi alla chiusura dello schermo.
Il quarto errore — non registrare i tentativi. Senza registrazione, non saprai quante richieste sono state ripetute, quali errori si sono verificati e quanto è efficace la tua Retry Policy. Aggiungi metriche: numero di tentativi, successo dopo il tentativo, distribuzione dei ritardi. Questi dati aiuteranno a ottimizzare i parametri della strategia per la tua applicazione specifica.
Domande frequenti
Il numero ottimale di tentativi è di 3–5 per la maggior parte degli scenari. Per la sincronizzazione in background, 5–7 tentativi sono accettabili; per richieste interattive (ad esempio, invio di moduli), non più di 3. Un numero maggiore di tentativi non aumenta la probabilità di successo ma consuma la batteria e il traffico dati dell'utente.
Exponential backoff — il raddoppio del ritardo tra i tentativi: 1 secondo, 2, 4, 8, 16 e così via. Se il server è sovraccarico, la breve pausa tra i primi tentativi gli consente di rispondere rapidamente, mentre la pausa crescente ad ogni tentativo successivo dà al server più tempo per riprendersi.
Ripetere solo errori temporanei: 408 (Request Timeout), 429 (Too Many Requests), 502 (Bad Gateway), 503 (Service Unavailable), 504 (Gateway Timeout). Gli errori 4xx (eccetto 408 e 429) indicano problemi del cliente — ripeterli non ha senso e può essere pericoloso per i dati dell'utente.
Retry Policy gestisce la ripetizione di una singola richiesta in caso di guasto. Il Circuit Breaker gestisce lo stato della connessione con un servizio: quando gli errori si accumulano, apre il circuito (OPEN) e blocca le nuove richieste. Il tentativo funziona a livello di singola chiamata, il Circuit Breaker a livello di integrazione del servizio.
Per testare la Retry Policy, utilizzare NetworkInterceptor (OkHttp) su Android e URLProtocol (URLSession) su iOS per simulare guasti di rete. Impostare parametri: frequenza degli errori, durata dell'indisponibilità e codici di risposta. I test unitari con MockWebServer (OkHttp) o OHHTTPStubs (iOS) verificano la logica di ripetizione senza rete reale.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche