Idempotentie in gedistribueerde systemen die echt werkt
Voorkom dubbele bijwerkingen
Idempotentie in gedistribueerde systemen is de eigenschap die je redt nadat het netwerk faalt, de wachtrij opnieuw probeert, de client paniekerig reageert en de operator een herhaling start. In productiesystemen is dubbele levering normaal. Dubbele bijwerkingen zijn de bug.
HTTP definieert een idempotente methode als een methode waarbij meerdere identieke verzoeken hetzelfde beoogde effect hebben op de server als één verzoek. Daarom zijn PUT, DELETE en veilige methoden idempotent in protocolsemantiek en kunnen ze automatisch worden opnieuw geprobeerd na een communicatiefout.

Die definitie is nuttig, maar niet voldoende. In echte architectuur is idempotentie geen triviaal HTTP-antwoord. Het is een zakelijke garantie. Als een klant één keer op “betalen” drukt, mag je niet twee keer afschrijven omdat er een time-out optrad tussen commit en antwoord. Als een worker de voorraad bijwerkt en crasht voordat het bericht bevestigd is, mag je de voorraad niet twee keer verlagen omdat de broker het bericht opnieuw heeft geleverd. Dat is de maatstaf.
De fout die ik keer op keer zie, is het behandelen van idempotentie als een transporteigenschap in plaats van een systeemeigenschap. Wachtrijdeduplicatie, HTTP-werkwoorden en clientherhalingen helpen, maar none van hen redden een ontwerp dat dezelfde zakelijke intentie laat leiden tot een tweede bijwerking. Als je de bredere context wilt zien van hoe deze integratiebeslissingen passen bij servicegrenzen en persistentieafwegingen, begin dan met App Architecture in Production: Integration Patterns, Code Design, and Data Access.
Waar duplicaten vandaan komen in productie
Duplicaten verschijnen niet omdat teams slordig zijn. Ze verschijnen omdat gedistribueerde systemen opnieuw proberen, herschikken en afspelen.
Een client kan een creatieverzoek verzenden, de server kan het commiten, en het antwoord kan toch verloren gaan op de lijn. Dat is precies waarom HTTP idempotente methoden onderscheidt en waarom betalings-API’s zoals Stripe en PayPal expliciete idempotentiemechanismen blootleggen voor onveilige methoden zoals POST.
Berichtbrokers maken het probleem nog duidelijker. At-least-once levering betekent dat een consument herhaaldelijk kan worden aangeroepen voor hetzelfde bericht, en een handler kan de database succesvol bijwerken maar falen voordat het bericht wordt bevestigd, waardoor de broker hetzelfde bericht opnieuw levert.
Webhooks zijn niet anders. GitHub zegt dat webhookleveringen in willekeurige volgorde kunnen aankomen, mislukte leveringen niet automatisch opnieuw worden geleverd, en elke levering een unieke X-GitHub-Delivery GUID bevat die je moet gebruiken bij bescherming tegen herhaling. Voor een praktische architectuurvisie op chat-endpoints als interactiegrenzen, zie Chat Platforms as System Interfaces in Modern Systems.
Zelfs systemen die sterkere garanties adverteren, laten je nog steeds werk over. Kafka kan dubbele entries in Kafka-logs voorkomen met idempotente producers en kan exactly-once levering bieden voor read-process-write flows die binnen Kafka blijven met transacties en read_committed consumers. Maar de eigen ontwerpdokumentatie van Kafka is duidelijk dat externe systemen nog steeds coördinatie nodig hebben met offsets en outputs. Google Cloud Pub/Sub exactly-once levering is beperkt tot pull-abonnementen, binnen een cloudregio, en vereist nog steeds dat clients de verwerkingsvoortgang bijhouden tot bevestiging slaagt.
Mijn meningvolle samenvatting is simpel. Ga er vanuit dat het transport zal opnieuw proberen. Ga er vanuit dat operators zullen herhalen. Ga er vanuit dat webhooks laat zullen aankomen. Ontwerp de schrijfpad zo dat een herhaalde intentie geen tweede zakelijke effect kan creëren.
Het API-contract waarin ik daadwerkelijk vertrouw
Hoe voorkomen idempotentiekeys dubbele API-verzoeken
Het enige API-contract waarin ik vertrouw voor muterende operaties is door de aanrover geleverde intentie plus serverzijds persistente opslag.
AWS adviseert een door de aanrover verstrekt verzoekidentificatienummer en waarschuwt dat de service het idempotentietoken atomiek moet registreren samen met het muterende werk. Stripe slaat de eerste statuscode en antwoordlichaam op voor een key, vergelijkt latere parameters met het oorspronkelijke verzoek, en retourneert hetzelfde resultaat voor herhalingen. PayPal gebruikt PayPal-Request-Id op ondersteunde POST-API’s en retourneert de laatste status voor het eerdere verzoek met dezelfde header.
Dat leidt tot een praktisch contract:
- De client genereert een idempotentiekey voor een zakelijke operatie.
- De server scopeert die key per tenant en operatienaam.
- De server slaat een verzoekhash op zodat dezelfde key niet kan worden hergebruikt voor een andere payload.
- De server registreert status zoals
pending,completed, offailed. - Herhalingen met dezelfde key retourneren ofwel het opgeslagen resultaat of een stabiele pointer ernaar.
- Herhalingen met dezelfde key en een andere payload falen luidruchtig.
Er is een IETF Idempotency-Key header-ontwerp, maar per 2026-05-09 staat het nog steeds in de IETF Datatracker als een verlopen Internet-Draft in plaats van een gepubliceerd RFC. In de praktijk is de headernaam nog steeds breed nuttig als een de facto conventie, maar je moet het contract documenteren in je eigen API in plaats te doen alsof de standaard af is.
Wat moet de key vertegenwoordigen? Intentie. Niet een HTTP-poging. Niet een TCP-verbinding. Niet een herhalingsteller. Als de gebruiker bedoelt “maak bestelling 123 één keer”, moet elke herhaling voor datzelfde commando dezelfde key hergebruiken. Als de gebruiker bedoelt “plaats een tweede bestelling”, moet dat een andere key gebruiken.
Een verzoek-ID is voor tracing. Een idempotentiekey is voor correctheid. Als je die verwart, zien je dashboards er netjes uit terwijl je geld twee keer beweegt.
Waarom PUT niet genoeg is
Nee, HTTP PUT is niet genoeg om een operatie idempotent te maken.
Ja, RFC 9110 geeft PUT idempotente semantiek. Maar als je PUT-handler een nieuw downstream-event uitstoot, een e-mail verstuurt bij elke herhaling, of een externe provider opnieuw afschrijft, dan heeft je implementatie het zakelijke contract geschonden, zelfs als je routenaam er respectabel uitziet.
Werkwoordkeuze helpt clients om intentie te begrijpen. Het implementeert niet intentie voor je.
Gebruik PUT wanneer het model van de resource echt past bij een volledige vervanging of upsert-stijl operatie. Gebruik POST wanneer je commando’s of acties creëert. Maar voor elke mutatie die mogelijk opnieuw geprobeerd kan worden over netwerkgrenzen heen, documenteer een expliciet idempotentiecontract. Als je muterende acties worden getriggerd vanuit chatworkflows, geldt hetzelfde contract in Slack Integration Patterns for Alerts and Workflows en Discord Integration Pattern for Alerts and Control Loops. Verborgen bijwerkingen zijn waar architectuur gaat sterven.
Hoe lang moet een idempotentiekey worden opgeslagen
Langer dan je transportteam wilt.
Stripe zegt dat keys kunnen worden verwijderd na minimaal 24 uur. PayPal zegt dat retentie API-specifiek is en geeft voorbeelden die tot 45 dagen kunnen duren. Amazon SQS FIFO dedupliceert alleen binnen een raam van 5 minuten. GitHub bewaart recente leveringen 3 dagen voor handmatige herlevering. Die cijfers zijn wild verschillend omdat de juiste retentieperiode een zakelijke beslissing is, geen protocolstandaard.
Als je keys slechts vijf minuten bewaart omdat je wachtrij dat doet, ontwerp je geen idempotentie. Je kopieert een transportbeperking naar je zakelijke laag.
Bewaar idempotentierecords voor minimaal het maximum van deze ruiten:
- client herhalingshorizon
- wachtrij herleveringshorizon
- webhook herhaalhorizon
- operator herhaalhorizon
- afrekening- of compensatiehorizon voor geldverschuivende operaties
Voor betalingen, boekingen en provisioning betekent dat vaak uren of dagen, niet minuten.
AWS noemt ook twee anti-patronen waar ik het volledig mee eens ben. Gebruik geen tijdstippen als key, omdat klokverschuiving en botsingen ze onbetrouwbaar maken. Sla niet blindeling volledige verzoekpayloads op als dedup-record voor elk verzoek, omdat dat prestaties en schaalbaarheid schaadt. Sla een genormaliseerde verzoekhash op plus de minimale antwoordstatus die je nodig hebt om veilig te herhalen. Als je het eerste antwoord byte voor byte moet reproduceren, sla dan het canonieke antwoordlichaam op zoals Stripe doet.
De databasepatronen die idempotentie echt maken
Idempotentie wordt echt wanneer de persistentielayer een race precies één keer kan winnen.
PostgreSQL geeft je twee kritieke primitives hier. Unieke constraints afdwingen van uniciteit op één of meer kolommen, en INSERT ... ON CONFLICT laat je een alternatieve actie definiëren in plaats te falen bij een uniciteitsviolatie. PostgreSQL documenteert ook dat ON CONFLICT DO UPDATE een atomiek insert-or-update resultaat garandeert onder concurrentie.
Dat betekent dat je idempotentie-laag meestal moet beginnen met een tabel zoals deze:
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)
);
En de behandelstroom er zo uit moet zien:
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
Het belangrijke onderdeel is niet de syntaxis. Het belangrijke onderdeel is de atomiciteit. Het registreren van de key en het uitvoeren van de mutatie moeten samen slagen of falen. AWS zegt dit expliciet voor API-idempotentie, en dezelfde regel geldt in SQL-achtergestelde services.
Doe geen naive check-then-act sequentie zoals “selecteer key; als ontbrekend dan voeg bestelling toe”. Onder concurrentie kunnen twee verzoeken de check passeren en beide de bijwerking creëren. Een unieke constraint is niet optioneel. Het is het mechanisme dat je architectuur omzet van optimistische folklore naar iets wat je onder belasting kunt bewijzen.
Hier is de regel die ik gebruik in reviews. Als het dedup-besluit niet wordt beschermd door dezelfde transactionele grens als de mutatie, heb je geen idempotentie. Je hebt hoop.
Berichten, events en webhooks hebben hun eigen grens nodig
Hoe gaan consumers om met dubbele events en berichten
Voor berichtconsumers is het klassieke patroon nog steeds het juiste. Registreer verwerkte bericht-ID’s in dezelfde database-transactie als de zakelijke update. Chris Richardson beschrijft de PROCESSED_MESSAGES tabelbenadering direct, met gebruik van een primaire key op subscriber en message ID zodat duplicaten schone falen en kunnen worden genegeerd.
Veel teams noemen die expliciete processed_messages store een inbox-tabel. De label is minder belangrijk dan de regel. De ontvanger moet bewijs persistent maken dat het het bericht al heeft verwerkt voordat een herhaling veilig niets kan doen.
Een minimale vorm ziet er zo uit:
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)
);
En de consumerstroom is net zo streng als de HTTP-stroom:
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
Dat patroon is saai. Goed. Idempotentie moet saai zijn.
Het is ook meestal beter dan proberen te leunen op broker marketingtermen. Kafka’s exactly-once ondersteuning is uitstekend wanneer je blijft binnen Kafka’s eigen transactionele model, maar Kafka’s docs waarschuwen nog steeds dat externe bestemmingen samenwerking nodig hebben. SQS FIFO vermindert dubbele verzendingen alleen binnen zijn 5-minuten dedup-raam. Pub/Sub exactly-once verwacht nog steeds dat de subscriber de voortgang bijhoudt en dubbel werk vermijdt wanneer bevestigingen falen.
Exactly-once is meestal een lokale optimalisatie. Idempotente bijwerkingen zijn de systeemgarantie.
Koppel dedup met het outbox-patroon
Als je service lokale status bijwerkt en ook een event publiceert, is idempotentieconsumptie alleen niet genoeg. Je hebt ook een veilige manier nodig om het event naar buiten te krijgen nadat de lokale transactie is ge-commit.
Daarom is het transactionele outbox-patroon belangrijk. Chris Richardson beschrijft het basisidee als het schrijven van het event naar een outbox-tabel in dezelfde transactie als de zakelijke update, en het vervolgens asynchroon publiceren. Debezium zegt dat het outbox-patroon inconsistenties vermijdt tussen de interne status van een service en de events die door andere services worden geconsumeerd. NServiceBus gaat verder en toont hoe outbox-verwerking inkomende berichten dedupliceert en zombie-records en ghost-berichten vermijdt.
Dit is de architectuur die ik aanbevel voor services die data eigenaarschap hebben en integratie-events publiceren:
- Valideer en persisteer het commando onder een idempotentiekey.
- Schrijf zakelijke status en outbox-event in één lokale transactie.
- Laat CDC of een outbox-dispatcher het event publiceren.
- Maak downstream consumers ook idempotent.
Outbox verwijdert niet de noodzaak voor idempotentieconsumers. Het verwijdert de noodzaak om te doen alsof een database-commit en een broker-publicatie één magische gedistribueerde transactie kunnen zijn wanneer ze dat meestal niet kunnen.
Webhooks zijn gewoon berichten met beter branding
Behandel inkomende webhooks precies als berichten van een onbetrouwbaar netwerkkant.
GitHub documenteert dat leveringen in willekeurige volgorde kunnen aankomen, adviseert het gebruik van X-Hub-Signature-256 om authenticiteit te verifiëren, en biedt X-GitHub-Delivery aan als het unieke leveringsidentificatienummer. Het merkt ook op dat herleveringen hetzelfde leverings-ID hergebruiken.
Dus de architectuur is straightforward:
- verifieer de handtekening eerst
- gebruik de leverings-GUID als dedup-key
- persisteer ontvangst voordat bijwerkingen plaatsvinden
- maak handlers volgordebewust in plaats ervan uit te gaan dat ze in volgorde aankomen
- queue het zware werk en keerp snel terug
Als je webhook-handler direct schrijft naar zakelijke tabellen voordat het ontvangst registreert, is het niet productieklaar. Het maakt alleen sneller dubbele fouten.
Sagas en workflow-engines hebben nog steeds idempotentie nodig
Sagas en durable workflow-engines verwijderen het probleem niet. Ze maken het zichtbaar.
Temporal adviseert om Activities idempotent te schrijven omdat Activities opnieuw geprobeerd kunnen worden na fouten of time-outs. Zijn docs noemen zelfs de edge-case waarbij een worker een externe bijwerking succesvol voltooit maar crasht voordat voltooiing wordt gerapporteerd, waardoor de Activity opnieuw loopt. Temporal suggereert ook om een combinatie van Workflow Run ID en Activity ID te gebruiken als stabiele idempotentiekey bij het aanroepen van downstream-services. Als je dit toepast in service-orchestratie, Go Microservices for AI/ML Orchestration behandelt de bredere workflow-afwegingen.
Dat is precies het juiste mentale model. Een workflow-engine kan uitvoeringgeschiedenis behouden en herhalingen coördineren. Het kan niet retroactief een kaart ontladen of een e-mail onverzenden tenzij je applicatie het idempotente stappen en idempotente compensaties geeft.
Hetzelfde geldt voor sagas. Temporal’s eigen saga-advies beschrijft compenserende acties die lopen wanneer een stap faalt. Die compensaties moeten ook idempotent zijn. Als “betaling terugstorten” twee keer loopt, heb je misschien de oorspronkelijke bug opgelost door er een nieuwe te creëren.
Mijn regel hier is brutaal en simpel. Elke Activity, elke command handler, en elke compensatie die de buitenwereld raakt, moet ofwel natuurlijk idempotent zijn of een echte idempotentiekey dragen naar het downstream-systeem.
Hoe te testen op idempotentie voordat productie
De meeste teams testen happy paths en doen dan verbaasd als herhalingen plaatsvinden. Dat is niet genoeg.
Je moet geautomatiseerde tests hebben voor minimaal deze gevallen:
- de server commit de mutatie maar het antwoord bereikt de client nooit
- twee identieke verzoeken racen met dezelfde idempotentiekey
- dezelfde key wordt hergebruikt met een andere payload
- een consumer commit zijn database werk en crasht voordat ack
- een webhook wordt afgespeeld met hetzelfde leverings-ID
- een outbox-dispatcher publiceert hetzelfde event meer dan één keer
- een workflow Activity voltooit de externe aanroep en crasht voordat voltooiing wordt gerapporteerd
- een idempotentie-record verloopt en een echte late herhaling arriveert
AWS adviseert expliciet uitgebreide testsuites die succesvolle verzoeken, mislukte verzoeken en dubbele verzoeken bevatten. Dat advies is pedestrian en absoluut correct.
Ik zou nog een failure drill toevoegen. Verifieer dat het afgespeelde antwoord semantiek equivalent is aan het eerste resultaat. AWS bespreekt laat-arriverende herhalingen en pleit voor antwoorden die de oorspronkelijke betekenis behouden zelfs nadat onderliggende status is veranderd. Dat is het verschil tussen “er is geen extra bijwerking gebeurd” en “de aanrover heeft nog steeds een consistent contract.”
Meningvolle regels die echte systemen redden
Hier zijn de regels die ik zou afdwingen in een architectuurreview.
Ten eerste, idempotentiekeys behoren tot zakelijke intentie, niet transportpogingen.
Ten tweede, scope elke key per tenant en operatie. Globale key-ruimtes zijn hoe ongerelateerde verzoeken botsen.
Ten derde, persisteer het dedup-besluit atomiek met de mutatie. Als dat niet waar is, is het ontwerp verkeerd.
Ten vierde, verwerp same-key different-payload herhalingen. Stripe en AWS doen dit beide om goede redenen.
Ten vijfde, bewaar keys voor de volledige herhaalhorizon van het zakelijke proces, niet voor de kortste wachtrijraam.
Ten zesde, koppel producers met een outbox en consumers met message-ID tracking. Één kant zonder de andere is half een ontwerp.
Ten zevende, propageer dezelfde operatie-identiteit downstream wanneer de zakelijke actie hetzelfde is. AWS adviseert expliciet om het idempotentietoken langs de verwerkingsketen door te geven.
Ten achtste, neem nooit aan dat exactly-once marketing de noodzaak verwijdert voor idempotente bijwerkingen.
Als dat streng klinkt, goed. Idempotentie is waar optimistische architectuur productie-realiteit ontmoet. Je hebt niet overal complexiteit nodig. Maar waar ook dubbele bijwerkingen geld, status of vertrouwen zouden schaden, moet idempotentie een first-class onderdeel van het contract zijn.