Naar de inhoud
maarten.
Alle stories

Idempotentie in API’s: dubbele verzoeken veilig verwerken

Maarten Soetens 12 min lezen

Een API-verzoek kan opnieuw worden verstuurd doordat een client een time-out krijgt, een gebruiker herhaald klikt of een netwerkverbinding wegvalt. Met idempotentie kan een API zulke herhalingen verwerken zonder dezelfde bewerking onbedoeld opnieuw uit te voeren. Dit artikel behandelt idempotency keys, opslag van verzoekstatus, gelijktijdige verzoeken en de grenzen van deze aanpak.

Wat idempotentie in een API wel en niet betekent

Een bewerking is idempotent als herhaling met dezelfde invoer uiteindelijk hetzelfde effect heeft als één uitvoering. Dat betekent niet noodzakelijk dat elke uitvoering identieke HTTP-responses oplevert. Een eerste verzoek kan bijvoorbeeld een resource aanmaken en een herhaling de bestaande resource teruggeven. Het relevante criterium is dat de onderliggende toestand niet telkens verder verandert.

HTTP-methodes geven een nuttige aanwijzing, maar zijn geen volledige implementatie. Een GET hoort geen toestand te wijzigen en een PUT vervangt doorgaans een resource door de opgegeven representatie. POST heeft meestal geen idempotent gedrag: twee identieke verzoeken kunnen twee bestellingen aanmaken. Een applicatie kan POST alsnog idempotent maken door verzoeken expliciet te identificeren en de uitkomst vast te leggen.

Idempotentie is ook iets anders dan exactly-once delivery. Een netwerk kan niet garanderen dat een client weet of een verzoek is aangekomen wanneer de verbinding uitvalt. De server kan de bewerking al hebben afgerond terwijl de response verloren gaat. Idempotente verwerking maakt herhalen veilig binnen een afgesproken scope, maar voorkomt niet dat transportlagen verzoeken dupliceren. De server moet daarom bepalen welke bewerkingen beschermd worden, wat als dezelfde bewerking geldt en hoelang die identiteit geldig blijft. Zonder die grenzen is idempotentie eerder een aanname dan een controleerbaar API-contract.

Waarom time-outs en retries dubbele bewerkingen veroorzaken

Een client weet bij een time-out niet vanzelf of de server het verzoek heeft ontvangen, verwerkt of zelfs al heeft afgerond. De response kan onderweg verloren zijn gegaan, terwijl de databasewijziging wel is vastgelegd. Een automatische retry is dan logisch vanuit beschikbaarheid, maar kan een tweede effect veroorzaken als de API geen herhalingen herkent. Bij een betaalverzoek kan dat leiden tot twee afschrijvingen; bij een formulier tot dubbele records of notificaties.

Duplicaten ontstaan niet alleen door expliciete retries in applicatiecode. Een mobiele verbinding kan wegvallen na verzending, een proxy kan een verzoek opnieuw aanbieden en een gebruiker kan na uitblijven van zichtbare feedback opnieuw op een knop drukken. Ook jobsystemen leveren berichten vaak minstens één keer af. Dat model is bewust gekozen: opnieuw afleveren is doorgaans veiliger dan een bericht stil verliezen. De ontvangende API moet dus rekening houden met herhaalde uitvoering.

Een retrybeleid hoort samen te werken met idempotentie. Exponentiële back-off en een limiet op het aantal pogingen verminderen piekbelasting, maar maken een muterende bewerking op zichzelf niet veilig. De client moet dezelfde idempotency key gebruiken bij elke poging van dezelfde logische actie. Een nieuwe key bij iedere retry vertelt de server juist dat er een nieuwe bewerking wordt aangevraagd. Leg bovendien vast wanneer een client opnieuw mag proberen, welke foutcodes retrybaar zijn en hoe lang de key bruikbaar blijft. Die afspraken voorkomen dat client en server verschillende ideeën hebben over één verzoek.

Idempotency keys kiezen en aan verzoeken koppelen

Een idempotency key is een unieke identificatie voor één logische mutatie. De client genereert de key voordat hij het verzoek verstuurt en hergebruikt die bij retries. Een UUID is vaak geschikt omdat de kans op botsingen klein is en clients geen centrale teller nodig hebben. De key hoort niet afgeleid te worden van alleen het account of een vaste actie: daarmee zouden latere, legitieme bewerkingen onterecht als duplicaat kunnen gelden.

De scope van een key is een belangrijke ontwerpkeuze. Een server kan keys uniek maken per API-client, gebruiker, endpoint of combinatie daarvan. Een brede scope maakt botsingen en onbedoelde koppeling tussen operaties waarschijnlijker; een te smalle scope kan dezelfde key op verschillende routes accepteren en onverwachte effecten geven. Een bruikbare sleutel bestaat daarom uit de key plus de identiteit van de aanroeper en de relevante bewerkingscontext. Authenticatie blijft noodzakelijk: een key is geen bewijs van toestemming.

De server moet ook vaststellen of dezelfde key met dezelfde inhoud is hergebruikt. Bewaar daarvoor bijvoorbeeld een hash van de methode, route en genormaliseerde requestbody. Als de key al bestaat maar de payload afwijkt, wijs het verzoek af in plaats van stil de oude uitkomst terug te geven of een nieuwe bewerking uit te voeren. Normalisatie vraagt aandacht voor betekenisvolle verschillen in velden, headers en bedragen. De key moet door de client als ondoorzichtige waarde worden behandeld; de server bepaalt de interpretatie en levensduur. Documenteer deze regels zodat SDK’s en integraties retries consistent uitvoeren.

Verzoekstatus opslaan zonder race conditions

Idempotentie vereist duurzame opslag van de relatie tussen key, verzoek en resultaat. Een eenvoudige registratie bevat doorgaans de scope, key, request-hash, status, responsegegevens en tijdstempels. Een unieke databaseconstraint op de gekozen scope en key is essentieel. Alleen eerst controleren of een key bestaat en daarna invoegen is niet veilig: twee gelijktijdige verzoeken kunnen allebei de controle passeren voordat een van beide schrijft.

Een gangbaar patroon is om de key atomair te claimen met een insert die door de unieke constraint wordt beschermd. Het verzoek dat de insert wint, mag de bewerking uitvoeren. Een tweede verzoek ziet dat de key al bestaat en leest de opgeslagen status. Als de eerste uitvoering nog loopt, kan de API wachten, een expliciete status teruggeven of een retrybare fout antwoorden. Die keuze hangt af van de verwachte duur en het protocol. Lang wachten houdt verbindingen bezet; onmiddellijk antwoorden vereist dat clients de status later kunnen opvragen of opnieuw proberen.

De registratie mag niet losstaan van de mutatie als een crash tussen beide een inconsistent resultaat kan achterlaten. Wanneer de bedrijfswijziging en de idempotency-status in dezelfde database staan, kan één transactie beide vastleggen. Bij een externe dienst is dat meestal niet mogelijk; daar zijn aparte statusovergangen en herstelprocessen nodig. Definieer ook hoe een record met status in uitvoering wordt hersteld na een procescrash. Een lease met vervaltijd kan een vastgelopen claim vrijgeven, maar vereist fencing of een vergelijkbare controle om te voorkomen dat een oude worker later alsnog tegelijk doorgaat.

Statusovergangen en opslag van de response

Een idempotency-record heeft meer nodig dan een aanduiding dat een key al is gezien. De server moet weten of de bewerking bezig is, succesvol is afgerond of definitief is mislukt. Veel implementaties gebruiken statussen als processing, completed en failed, met gecontroleerde overgangen ertussen. Een onvolledige registratie mag niet automatisch als afgerond worden behandeld: dat kan een client een succesvolle uitkomst tonen terwijl de bedrijfsbewerking nooit heeft plaatsgevonden.

Na voltooiing kan de API de oorspronkelijke statuscode en relevante responsebody bewaren. Een retry ontvangt dan dezelfde uitkomst zonder de mutatie opnieuw uit te voeren. Bij grote responses is het vaak beter alleen noodzakelijke velden of een verwijzing naar het resultaat op te slaan. Let op headers met dynamische waarden, zoals request-ID’s, timestamps of cookies. Het letterlijk herhalen van alle headers kan onjuist zijn; bepaal welke responsegegevens onderdeel zijn van het contract en welke bij iedere HTTP-uitwisseling opnieuw worden gegenereerd.

Fouten vragen een expliciete beleidskeuze. Een validatiefout voordat er een mutatie plaatsvond kan soms opnieuw worden berekend, maar dan moet duidelijk zijn of dezelfde key met gewijzigde invoer is toegestaan. Een fout nadat een externe actie mogelijk al is uitgevoerd, mag niet zonder controle leiden tot een nieuwe uitvoering. Sla de foutstatus op als de uitkomst definitief is, en gebruik een aparte herstelstatus wanneer die nog onzeker is. Maak in responses onderscheid tussen een lopende bewerking en een afgehandelde mislukking. Zo kan een client passend reageren zonder zelf te gokken op basis van een algemene serverfout.

Database-transacties en gelijktijdige verzoeken

Een unieke key voorkomt dat twee verzoeken allebei als nieuwe bewerking worden geregistreerd, maar de volgorde van databasehandelingen blijft bepalend. De keyclaim, bedrijfswijziging en markering als voltooid moeten waar mogelijk binnen één transactie vallen. Als het proces tussen die stappen crasht, zorgt rollback ervoor dat de database geen half afgemaakte combinatie bewaart. Dit patroon werkt vooral goed wanneer de mutatie volledig binnen dezelfde relationele database plaatsvindt.

Een transactie die tijdens een lange berekening of netwerkcall openblijft, houdt locks vast en vergroot de kans op blokkades. Scheid daarom korte databasebewerkingen van langlopende verwerking. Een alternatief is eerst een record in status processing aan te maken en daarna de bewerking uit te voeren. Dat vereist wel herstelgedrag voor workers die wegvallen, plus bescherming tegen een tweede worker die dezelfde key oppakt. Leases, transactietokens en compare-and-swap-updates kunnen helpen, maar moeten als één consistent protocol ontworpen worden.

Bij hoge belasting kan dezelfde key vrijwel gelijktijdig op meerdere applicatie-instanties binnenkomen. Een lokale mutex beschermt alleen één proces en is dus onvoldoende in een horizontaal geschaalde omgeving. De databaseconstraint of een gedeelde coördinatielaag moet de autoriteit zijn. Redis kan voor snelle coördinatie bruikbaar zijn, maar een vluchtige cache alleen is riskant wanneer verlies van data een dubbele financiële of operationele actie mogelijk maakt. Denk ook aan isolatieniveau en deadlocks. Behandel een conflict niet als een willekeurige serverfout; vertaal het naar een herhaalbaar API-resultaat en log voldoende context om contention te onderscheiden van defecte statusovergangen.

Externe effecten, message queues en outboxpatronen

Een database-transactie kan niet tegelijk een externe betaalprovider, e-maildienst of berichtbroker atomair aansturen. Als de API eerst de database commit en daarna een provider aanroept, kan een crash tussen die stappen de externe actie overslaan. Draait de volgorde andersom, dan kan de provider de actie uitvoeren terwijl de database de uitkomst niet registreert. Een retry kan vervolgens een tweede extern effect veroorzaken, ook wanneer de eigen API idempotent lijkt.

Geef daarom waar mogelijk ook aan de downstreamdienst een stabiele idempotency key door. Gebruik een afgeleide sleutel die dezelfde logische operatie identificeert, niet een nieuwe waarde per poging. Controleer de semantiek van de provider: hoe lang bewaart die keys, welke velden vergelijkt hij en wat gebeurt er bij dezelfde key met een andere payload? Als een externe dienst geen idempotentie ondersteunt, kan een statusopvraag of reconciliatie nodig zijn voordat de actie opnieuw wordt gestart.

Voor databasewijzigingen die een bericht moeten opleveren, voorkomt het outboxpatroon dat commit en publish uit elkaar lopen. De applicatie schrijft bedrijfsdata en een outboxrecord in één transactie; een aparte worker publiceert het bericht en markeert het record daarna als verwerkt. Omdat publish en markering zelf kunnen overlappen, moet ook de consument duplicaten verdragen, bijvoorbeeld met een unieke event-ID en een inboxregistratie. Dit is geen exactly-once-keten, maar een reeks expliciete, herstelbare stappen. Houd per stap de correlatie-ID en status bij, zodat een onzekere uitkomst onderzocht kan worden voordat een potentieel schadelijke actie opnieuw wordt uitgevoerd.

Vervaltijd, beveiliging en tests voor idempotentie

Idempotency records onbeperkt bewaren is kostbaar en kan onnodig gevoelige request- of responsegegevens vasthouden. Te vroeg verwijderen is echter ook riskant: een vertraagde retry kan dan opnieuw als nieuwe bewerking worden uitgevoerd. Kies de bewaartermijn op basis van het retrygedrag van clients, de verwerkingstijd van queues en de impact van een duplicaat. Voor kritieke mutaties kan de bedrijfsdatabase daarnaast een blijvende unieke referentie nodig hebben, los van de tijdelijke opslag voor responses.

Beperk opgeslagen gegevens tot wat nodig is om identiteit en uitkomst te controleren. Een request-hash kan inhoud vergelijken zonder de volledige body te bewaren, al vraagt hashing om consistente canonicalisatie en biedt een hash geen bescherming tegen misbruik als de invoer voorspelbaar is. Bescherm idempotency records met dezelfde toegangscontrole als andere bedrijfsdata. Behandel sleutels bovendien niet als geheimen of autorisatietokens. Rate limits en limieten op keylengte en payloadgrootte voorkomen dat de opslag een eenvoudig doelwit wordt voor uitputting.

Tests moeten gedrag onder herhaling en gelijktijdigheid aantonen, niet alleen de normale route controleren. Test dezelfde key met identieke inhoud, dezelfde key met afwijkende inhoud, twee gelijktijdige verzoeken, een client-time-out na commit en een procescrash tussen statusovergangen. Controleer ook dat verlopen records het bedoelde gedrag hebben en dat responsegegevens consistent terugkomen. In productie zijn metrics voor duplicaatverzoeken, conflicten, vastgelopen processing-records en downstreamretries nuttig. Log keys bij voorkeur gepseudonimiseerd of beperkt, zodat correlatie mogelijk blijft zonder onnodige gegevens bloot te leggen. Deze signalen maken zichtbaar of het gekozen contract aansluit op het werkelijke retrygedrag van clients en infrastructuur.

Veelgestelde vragen

Hoe lang moet een API een idempotency key bewaren?

Bewaar een idempotency key minstens zo lang als clients, jobs en tussenliggende systemen dezelfde bewerking nog kunnen opnieuw aanbieden. Bepaal die termijn aan de hand van de maximale retryperiode, vertraagde berichten en de gevolgen van een dubbele uitvoering. Een korte termijn verlaagt opslagkosten, maar kan een late retry veranderen in een nieuwe bewerking. Voor risicovolle acties kan aanvullende deduplicatie op basis van een bedrijfskenmerk nodig zijn, ook nadat het idempotency-record is verlopen.

Hoe behoudt een mobiele app dezelfde idempotency key na een herstart?

Een mobiele app behoudt dezelfde idempotency key door die samen met de nog niet afgeronde actie lokaal op te slaan. Als de app sluit of de verbinding wegvalt, kan zij de actie hervatten met dezelfde sleutel en dezelfde inhoud. Verwijder de opgeslagen actie pas wanneer de server een definitieve uitkomst heeft bevestigd of wanneer de gebruiker de actie expliciet annuleert. Maak voor een nieuwe handeling altijd een nieuwe sleutel, ook als de gegevens toevallig identiek zijn.

Welke HTTP-statuscode geef je terug als hetzelfde verzoek nog wordt verwerkt?

Geef bij een verzoek dat nog wordt verwerkt een response die duidelijk maakt dat de uitkomst nog niet definitief is, bijvoorbeeld 202 Accepted met een manier om de status op te vragen. Als het protocol geen statusendpoint heeft, kan een tijdelijke fout met een Retry-After-header passend zijn. Kies één gedrag en documenteer het, zodat clients niet zelf hoeven te raden. Gebruik een definitieve succes- of foutstatus pas wanneer de uitkomst van de bewerking vaststaat.

Is een idempotency key een geheim dat een client moet beschermen?

Een idempotency key is op zichzelf geen geheim en verleent geen toegang tot een account of bewerking. De API moet bij elk verzoek de normale authenticatie en autorisatie controleren. Behandel de sleutel wel als ondoorzichtige invoer: neem er geen persoonsgegevens of betaalgegevens in op, accepteer alleen een redelijke lengte en log hem niet onnodig. Zo beperk je misbruik, onbedoelde datalekken en opslagproblemen, terwijl de sleutel gewoon zijn functie als herkenningstoken behoudt.

Hoe maak je een batch-API idempotent als sommige items mislukken?

Maak bij een batch-API expliciet of de batch atomair is of dat ieder item afzonderlijk kan slagen. Bij een atomaire batch hoort één sleutel bij de volledige batch en moet een retry de vastgelegde batchuitkomst opleveren. Bij gedeeltelijke verwerking is een sleutel per item vaak geschikter, zodat mislukte items opnieuw kunnen worden aangeboden zonder geslaagde items opnieuw uit te voeren. Neem in de response per item een stabiele identificatie en status op en leg vast hoe gewijzigde items worden behandeld.

Portret van Maarten

Maarten

Freelance developer in Nijmegen

Even kennismaken?

Vertel kort wat er speelt. Dan hoor je wat er kan, wat ik anders zou doen en waar AI bij jou wél en niet iets toevoegt. Vrijblijvend.

[email protected]
Het kantoor in Nijmegen
© 2026 maarten.online Sitemap Privacy Algemene voorwaarden
Het kantoor in Nijmegen

Maarten.

Freelance developer in Nijmegen. Liever direct contact? Dat kan ook.

Kennismaken

Laat je gegevens achter, dan kijken we of het klikt. Vrijblijvend en zonder verkooppraat.

Maarten

Stuur een bericht via WhatsApp

Hoi! Waar kan ik je mee helpen?

nu