Idempotenza nei sistemi distribuiti che funziona davvero
Evita effetti collaterali duplicati
L’idempotenza nei sistemi distribuiti è la proprietà che ti salva quando la rete mente, la coda ritenta, il client entra in panico e l’operatore esegue un replay. Nei sistemi di produzione, la consegna duplicata è normale. Gli effetti collaterali duplicati sono il bug.
HTTP definisce un metodo idempotente come uno in cui più richieste identiche hanno lo stesso effetto previsto sul server di una singola richiesta. Ecco perché PUT, DELETE e i metodi sicuri sono idempotenti nella semantica del protocollo e possono essere ritentati automaticamente dopo un fallimento di comunicazione.

Questa definizione è utile, ma non è sufficiente. Nelle architetture reali, l’idempotenza non è una risposta da trivia HTTP. È una garanzia di business. Se un cliente preme “paga” una volta, non puoi caricarlo due volte perché si è verificato un timeout tra il commit e la risposta. Se un worker aggiorna l’inventario e crasha prima di confermare il messaggio, non puoi decrementare lo stock due volte perché il broker ha ridistribuito il messaggio. Questo è lo standard.
L’errore che vedo ripetutamente è trattare l’idempotenza come una caratteristica di trasporto invece che come una proprietà del sistema. La deduplicazione delle code, i verbi HTTP e i ritentativi del client aiutano, ma nessuno di loro salva un design che consente alla stessa intenzione di business di creare un secondo effetto collaterale. Se vuoi un quadro più ampio su come queste decisioni di integrazione si adattano ai confini dei servizi e ai compromessi di persistenza, inizia con Architettura delle App in Produzione: Pattern di Integrazione, Design del Codice e Accesso ai Dati.
Da dove provengono i duplicati in produzione
I duplicati non appaiono perché i team sono negligenti. Appaiono perché i sistemi distribuiti ritentano, riordinano e riproducono.
Un client può inviare una richiesta di creazione, il server può eseguirne il commit, e la risposta può comunque scomparire in rete. Ecco esattamente perché HTTP distingue i metodi idempotenti e perché le API di pagamento come Stripe e PayPal espongono meccanismi di idempotenza espliciti per metodi non sicuri come POST.
I broker di messaggi rendono il problema ancora più evidente. La consegna almeno una volta (at-least-once) significa che un consumatore può essere invocato ripetutamente per lo stesso messaggio, e un gestore può aggiornare il database con successo ma fallire prima del riconoscimento, causando al broker la ridistribuzione dello stesso messaggio.
I webhook non sono diversi. GitHub afferma che le consegne dei webhook possono arrivare in ordine errato, le consegne fallite non vengono ridistribuite automaticamente e ogni consegna trasporta un GUID unico X-GitHub-Delivery che dovresti utilizzare per proteggerti dal replay. Per una visione architettonica pratica degli endpoint di chat come confini di interazione, vedi Piattaforme Chat come Interfacce di Sistema nei Sistemi Moderni.
Anche i sistemi che pubblicizzano garanzie più forti ti lasciano comunque del lavoro da fare. Kafka può prevenire voci duplicate nei log di Kafka con produttori idempotenti e può fornire consegna esattamente una volta (exactly-once) per flussi read-process-write che restano all’interno di Kafka con transazioni e consumatori read_committed. Ma i documenti di design di Kafka stessi sono chiari nel dire che i sistemi esterni richiedono ancora coordinamento con offset e output. La consegna esattamente una volta di Google Cloud Pub/Sub è limitata alle sottoscrizioni pull, all’interno di una regione cloud e richiede ancora ai client di tracciare il progresso dell’elaborazione fino a quando il riconoscimento non ha successo.
Il mio riassunto opinionated è semplice. Assumi che il trasporto ritenterà. Assumi che gli operatori eseguiranno il replay. Assumi che i webhook arriveranno in ritardo. Progetta il percorso di scrittura in modo che un’intenzione ripetuta non possa creare un secondo effetto di business. Il design degli errori è strettamente correlato: come gli errori vengono incapsulati, tradotti e classificati come ritentabili o non ritentabili fa parte della stessa disciplina dei confini — Architettura di Gestione degli Errori in Go: Confini e Pattern copre la classificazione degli errori ritentabili, la traduzione dei confini e i pattern sentinella che permettono alla logica di retry di prendere decisioni sensate. Quando i ritentativi continuano a colpire una dipendenza non sana, un interruttore di circuito (circuit breaker) al confine di integrazione fallisce rapidamente prima che le tempeste di retry amplifichino il lavoro duplicato.
Il contratto API in cui fidarmi realmente
Come le chiavi di idempotenza prevengono le richieste API duplicate
L’unico contratto API in cui mi fido per le operazioni di mutazione è l’intenzione fornita dal chiamante più la persistenza lato server.
AWS raccomanda un identificatore di richiesta fornito dal chiamante e avvisa che il servizio deve registrare atomicamente il token di idempotenza insieme al lavoro di mutazione. Stripe memorizza il primo codice di stato e il corpo della risposta per una chiave, confronta i parametri successivi con la richiesta originale e restituisce lo stesso risultato per i ritentativi. PayPal usa PayPal-Request-Id sulle API POST supportate e restituisce lo stato più recente per la richiesta precedente con lo stesso header.
Questo porta a un contratto pratico:
- Il client genera una chiave di idempotenza per un’operazione di business.
- Il server scopa (scopes) quella chiave per tenant e nome dell’operazione.
- Il server memorizza un hash della richiesta in modo che la stessa chiave non possa essere riutilizzata per un payload diverso.
- Il server registra lo stato come
in corso(pending),completatoofallito. - I ritentativi con la stessa chiave restituiscono l’esito memorizzato o un puntatore stabile ad esso.
- I ritentativi con la stessa chiave e un payload diverso falliscono in modo esplicito (loudly).
Esiste una bozza IETF per l’header Idempotency-Key, ma al 09-05-2026 è ancora elencata nel IETF Datatracker come Internet-Draft scaduta piuttosto che come RFC pubblicata. Nella pratica, il nome dell’header è ancora ampiamente utile come convenzione de facto, ma dovresti documentare il contratto nella tua API invece di fingere che lo standard sia finito.
Cosa dovrebbe rappresentare la chiave? L’intenzione. Non un tentativo HTTP. Non una connessione TCP. Non un contatore di retry. Se l’utente intende “crea ordine 123 una volta”, ogni retry per quello stesso comando deve riutilizzare la stessa chiave. Se l’utente intende “effettua un secondo ordine”, deve usare una chiave diversa.
Un ID richiesta è per il tracing. Una chiave di idempotenza è per la correttezza. Se li confondi, i tuoi dashboard sembrano ordinati mentre il tuo denaro si muove due volte.
Perché PUT non è sufficiente
No, HTTP PUT non è sufficiente per rendere un’operazione idempotente.
Sì, RFC 9110 assegna a PUT semantica idempotente. Ma se il tuo handler PUT emette un nuovo evento a valle, invia un’email ad ogni retry o carica un provider esterno di nuovo, allora la tua implementazione ha violato il contratto di business anche se il nome della tua rotta sembra rispettabile.
La scelta del verbo aiuta i client a comprendere l’intenzione. Non implementa l’intenzione per te.
Usa PUT quando il modello di risorse si adatta genuinamente a un’operazione di sostituzione completa o stile upsert. Usa POST quando stai creando comandi o azioni. Ma per qualsiasi mutazione che potrebbe essere ritentata oltre i confini di rete, documenta un contratto di idempotenza esplicito. Se le tue azioni di mutazione sono attivate da flussi di lavoro di chat, lo stesso contratto si applica in Pattern di Integrazione Slack per Alert e Flussi di Lavoro e Pattern di Integrazione Discord per Alert e Loop di Controllo. Gli effetti collaterali nascosti sono dove l’architettura va a morire.
Per quanto tempo dovrebbe essere memorizzata una chiave di idempotenza
Più a lungo di quanto il tuo team di trasporto voglia.
Stripe dice che le chiavi possono essere eliminate dopo almeno 24 ore. PayPal dice che la ritenzione è specifica per l’API e fornisce esempi che possono durare fino a 45 giorni. Amazon SQS FIFO deduplica solo entro una finestra di 5 minuti. GitHub conserva le consegne recenti per 3 giorni per la ridistribuzione manuale. Questi numeri sono wildly diversi perché il periodo di ritenzione corretto è una decisione di business, non un default del protocollo.
Se conservi le chiavi solo per cinque minuti perché la tua coda lo fa, non stai progettando l’idempotenza. Stai copiando una limitazione di trasporto nel tuo livello di business.
Conserva i record di idempotenza per almeno il massimo di queste finestre:
- orizzonte di retry del client
- orizzonte di redrive della coda
- orizzonte di replay dei webhook
- orizzonte di replay dell’operatore
- orizzonte di regolamento o compensazione per operazioni che muovono denaro
Per pagamenti, prenotazioni e provisioning, questo spesso significa ore o giorni, non minuti.
AWS evidenzia anche due anti-pattern con cui sono pienamente d’accordo. Non usare timestamp come chiave, perché lo skew dell’orologio e le collisioni li rendono inaffidabili. Non memorizzare ciecamente payload di richiesta interi come record di dedup per ogni richiesta, perché ciò danneggia le prestazioni e la scalabilità. Memorizza un hash di richiesta normalizzato più lo stato di risposta minimo necessario per riprodurre in modo sicuro. Se devi riprodurre la prima risposta byte per byte, memorizza il corpo della risposta canonica come fa Stripe.
I pattern del database che rendono reale l’idempotenza
L’idempotenza diventa reale quando lo strato di persistenza può vincere una gara esattamente una volta.
PostgreSQL ti offre due primitive critiche qui. I vincoli univoci (unique constraints) applicano l’unicità su una o più colonne, e INSERT ... ON CONFLICT ti permette di definire un’azione alternativa invece di fallire su una violazione di unicità. PostgreSQL documenta anche che ON CONFLICT DO UPDATE garantisce un risultato atomico insert-or-update sotto concorrenza.
Questo significa che il tuo strato di idempotenza dovrebbe solitamente iniziare con una tabella come questa:
create table api_idempotency (
tenant_id text not null,
operation text not null,
idempotency_key text not null,
request_hash text not null,
state text not null,
status_code integer,
response_body jsonb,
resource_type text,
resource_id text,
created_at timestamptz not null default now(),
expires_at timestamptz not null,
primary key (tenant_id, operation, idempotency_key)
);
E il flusso di gestione dovrebbe essere simile a questo:
begin transaction
try insert (tenant_id, operation, idempotency_key, request_hash, state='pending')
on conflict do nothing
load row for (tenant_id, operation, idempotency_key) for update
if row.request_hash != incoming_request_hash
fail with conflict or validation error
if row.state = 'completed'
return stored response
if row.state = 'pending' and row was created by another live request
either wait briefly, or fail fast with a retryable response
perform local business mutation
store stable result in idempotency row
set state = 'completed'
commit
return result
La parte importante non è la sintassi. La parte importante è l’atomicità. La registrazione della chiave e l’esecuzione della mutazione devono avere successo o fallire insieme. AWS lo dice esplicitamente per l’idempotenza API, e la stessa regola si applica nei servizi supportati da SQL.
Non fare una sequenza naive di check-then-act come “select key; if missing then insert order”. Sotto concorrenza, due richieste possono passare il check e entrambe creare l’effetto collaterale. Un vincolo univoco non è opzionale. È il meccanismo che trasforma la tua architettura da folklore ottimistico a qualcosa che puoi dimostrare sotto carico.
Ecco la regola che uso nelle recensioni. Se la decisione di dedup non è protetta dallo stesso confine transazionale della mutazione, non hai idempotenza. Hai speranza.
Messaggi, eventi e webhook hanno bisogno del proprio confine
Come i consumatori gestiscono eventi e messaggi duplicati
Per i consumatori di messaggi, il pattern classico è ancora quello giusto. Memorizza gli ID dei messaggi elaborati nella stessa transazione del database dell’aggiornamento del business. Chris Richardson descrive direttamente l’approccio della tabella PROCESSED_MESSAGES, usando una chiave primaria su subscriber e ID messaggio in modo che i duplicati falliscano in modo pulito e possano essere ignorati.
Molti team chiamano questo store esplicito processed_messages una tabella inbox. L’etichetta importa meno della regola. Il ricevente deve persistere la prova che ha già gestito il messaggio prima che un retry possa fare nulla in modo sicuro.
Una forma minimale sembra così:
create table processed_messages (
subscriber_id text not null,
message_id text not null,
processed_at timestamptz not null default now(),
primary key (subscriber_id, message_id)
);
E il flusso del consumatore è altrettanto rigoroso del flusso HTTP:
begin transaction
insert into processed_messages (subscriber_id, message_id)
values (?, ?)
on conflict do nothing
if no row inserted
rollback
ack and ignore duplicate
apply business mutation
commit
ack message
Quel pattern è noioso. Bene. L’idempotenza dovrebbe essere noiosa.
È anche solitamente migliore rispetto al tentativo di affidarsi a termini di marketing dei broker. Il supporto exactly-once di Kafka è eccellente quando resti all’interno del proprio modello transazionale di Kafka, ma i documenti di Kafka avvertono ancora che le destinazioni esterne hanno bisogno di cooperazione. SQS FIFO riduce i invii duplicati solo entro la sua finestra di dedup di 5 minuti. Pub/Sub exactly-once si aspetta ancora che il subscriber tracci il progresso ed eviti il lavoro duplicato quando i riconoscimenti falliscono.
Exactly-once è solitamente un’ottimizzazione locale. Gli effetti collaterali idempotenti sono la garanzia del sistema.
Abbina dedup con il pattern outbox
Se il tuo servizio aggiorna lo stato locale e pubblica anche un evento, il consumo idempotente da solo non è sufficiente. Hai anche bisogno di un modo sicuro per far uscire l’evento dopo che la transazione locale ha eseguito il commit.
Ecco perché il pattern outbox transazionale è importante. Chris Richardson descrive l’idea di base come scrivere l’evento in una tabella outbox nella stessa transazione dell’aggiornamento del business, e poi pubblicarlo asincronamente. Debezium dice che il pattern outbox evita incoerenze tra lo stato interno di un servizio e gli eventi consumati da altri servizi. NServiceBus va oltre e mostra come l’elaborazione outbox deduplica i messaggi in entrata ed evita record zombie e messaggi fantasma.
Questa è l’architettura che raccomando per servizi che possiedono dati e pubblicano eventi di integrazione:
- Convalida e persisti il comando sotto una chiave di idempotenza.
- Scrivi lo stato del business e l’evento outbox in una singola transazione locale.
- Lascia che CDC o un dispatcher outbox pubblichi l’evento.
- Rendi idempotenti anche i consumatori a valle.
Outbox non rimuove la necessità di consumatori idempotenti. Rimuove la necessità di fingere che un commit del database e una pubblicazione del broker possano essere una magica transazione distribuita quando solitamente non possono.
I webhook sono solo messaggi con un branding migliore
Tratta i webhook in entrata esattamente come messaggi da un bordo di rete non fidato.
GitHub documenta che le consegne possono arrivare in ordine errato, raccomanda l’uso di X-Hub-Signature-256 per verificare l’autenticità e fornisce X-GitHub-Delivery come identificatore unico di consegna. Nota anche che le ridistribuzioni riutilizzano lo stesso ID di consegna.
Quindi l’architettura è semplice:
- verifica la firma per prima
- usa il GUID di consegna come chiave di dedup
- persisti la ricevuta prima degli effetti collaterali
- rendi gli handler consapevoli dell’ordine invece di assumere l’ordine di arrivo
- metti in coda il lavoro pesante e restituisci velocemente
Se il tuo handler webhook scrive direttamente nelle tabelle di business prima di registrare la ricevuta, non è pronto per la produzione. È solo più veloce nel commettere errori duplicati.
Saga e motori di workflow hanno ancora bisogno di idempotenza
Le saga e i motori di workflow duraturi non eliminano il problema. Lo rendono visibile.
Temporal raccomanda di scrivere le Attività (Activities) in modo idempotente perché le Attività possono essere ritentate dopo fallimenti o timeout. I suoi documenti evidenziano persino il caso limite in cui un worker completa un effetto collaterale esterno con successo ma crasha prima di segnalare il completamento, causando l’esecuzione dell’Attività di nuovo. Temporal suggerisce anche di usare una combinazione di Workflow Run ID e Activity ID come chiave di idempotenza stabile quando si chiamano servizi a valle. Se stai applicando questo nell’orchestrazione dei servizi, Microservizi Go per l’Orchestrazione AI/ML copre i compromessi di workflow più ampi.
Questo è esattamente il modello mentale giusto. Un motore di workflow può preservare la storia dell’esecuzione e coordinare i ritentativi. Non può retroattivamente annullare l’addebito di una carta o annullare l’invio di un’email a meno che la tua applicazione non gli dia passaggi idempotenti e compensazioni idempotenti.
Lo stesso si applica alle saga. La guida alle saga di Temporal stessa descrive azioni compensative che si eseguono quando un passaggio fallisce. Quelle compensazioni devono essere idempotenti anche loro. Se “rimborsa pagamento” viene eseguito due volte, potresti aver risolto il bug originale creando un nuovo bug.
La mia regola qui è brutale e semplice. Ogni Attività, ogni handler di comando, e ogni compensazione che tocca il mondo esterno dovrebbe essere naturalmente idempotente o trasportare una vera chiave di idempotenza al sistema a valle.
Come testare l’idempotenza prima della produzione
La maggior parte dei team testa i percorsi felici (happy paths) e poi si mostra sorpresa quando accadono i ritentativi. Questo non è sufficiente. Per i team Go, Test del Codice Concorrente Go con testing/synctest copre come scrivere test veloci e deterministici per loop di retry e comportamento di deadline del contesto senza dormire attraverso ritardi artificiali.
Dovresti avere test automatizzati per almeno questi casi:
- il server esegue il commit della mutazione ma la risposta non raggiunge mai il client
- due richieste identiche gareggiano con la stessa chiave di idempotenza
- la stessa chiave viene riutilizzata con un payload diverso
- un consumatore esegue il commit del lavoro del database e crasha prima di ack
- un webhook viene riprodotto con lo stesso ID di consegna
- un dispatcher outbox pubblica lo stesso evento più di una volta
- un’Attività di workflow completa la chiamata esterna e crasha prima che il completamento venga segnalato
- un record di idempotenza scade e un vero retry tardivo arriva
AWS raccomanda esplicitamente suite di test complete che includano richieste di successo, richieste fallite e richieste duplicate. Quel consiglio è pedestre e assolutamente corretto.
Aggiungerei un altro drill di fallimento. Verifica che la risposta riprodotta sia semanticamente equivalente al primo risultato. AWS discute i retry che arrivano in ritardo e argomenta per risposte che preservino il significato originale anche dopo che lo stato sottostante è cambiato. Questa è la differenza tra “non è accaduto alcun effetto collaterale extra” e “il chiamante ha ancora un contratto consistente”.
Regole opinionated che salvano sistemi reali
Ecco le regole che imporrei in una revisione architetturale.
Primo, le chiavi di idempotenza appartengono all’intenzione di business, non ai tentativi di trasporto.
Secondo, scopa (scope) ogni chiave per tenant e operazione. Gli spazi delle chiavi globali sono il modo in cui richieste non correlate collidono.
Terzo, persisti la decisione di dedup atomicamente con la mutazione. Se non è vero, il design è sbagliato.
Quarto, rifiuta i retry con stessa chiave e payload diverso. Stripe e AWS lo fanno entrambi per una buona ragione.
Quinto, conserva le chiavi per l’intero orizzonte di replay del processo di business, non per la finestra di coda più corta.
Sesto, abbina i produttori con un outbox e i consumatori con il tracciamento degli ID dei messaggi. Un lato senza l’altro è metà design.
Settimo, propaga la stessa identità dell’operazione a valle quando l’azione di business è la stessa. AWS raccomanda esplicitamente di passare il token di idempotenza lungo la catena di elaborazione.
Ottavo, non assumere mai che il marketing exactly-once rimuova la necessità di effetti collaterali idempotenti.
Se questo suona rigoroso, bene. L’idempotenza è dove l’architettura ottimistica incontra la realtà della produzione. Non hai bisogno di complessità ovunque. Ma ovunque effetti collaterali duplicati danneggerebbero denaro, stato o fiducia, l’idempotenza dovrebbe essere una parte di prima classe del contratto.
Queste stesse regole si applicano direttamente agli agenti AI in background. Gli agenti di polling che reclamano task, emettono notifiche o attivano chiamate agli strumenti hanno bisogno di chiavi di dedup e protocolli di claim idempotenti tanto quanto le API di pagamento. Per capire come funziona il pattern claim-and-dedupe all’interno degli assistenti AI di produzione, vedi Agenti di Polling negli Assistenti AI: 11 Pattern di Implementazione.