Checklist voor webhook-betrouwbaarheid: retries, idempotentie, replay
Praktische checklist voor webhook-betrouwbaarheid: retries, idempotentie, replay-logs en monitoring voor inkomende en uitgaande webhooks wanneer partners falen.

Waarom webhooks onbetrouwbaar lijken in echte projecten
Een webhook is simpel: het ene systeem stuurt een HTTP-aanvraag naar een ander systeem als er iets gebeurt. "Order verzonden", "ticket geüpdatet", "apparaat offline". Het is in wezen een push-notificatie tussen apps, geleverd via het web.
Ze voelen betrouwbaar aan in demo's omdat het gelukkige pad snel en schoon is. In echt werk zitten webhooks tussen systemen die je niet beheert: CRM's, vervoerders, helpdesks, marketingtools, IoT-platforms en zelfs interne apps van een ander team. Buiten betalingen heb je vaak geen volwassen leveringsgaranties, stabiele event-schema's of consistente retry-gedragingen.
De eerste signalen zijn meestal verwarrend:
- Dubbele events (dezelfde update komt twee keer binnen)
- Missende events (er is iets veranderd, maar je hebt er nooit iets van gehoord)
- Vertragingen (een update arriveert minuten of uren later)
- Events buiten volgorde (een "closed" update arriveert vóór "opened")
Wankele derde-partijsystemen maken dit willekeurig omdat fouten niet altijd luid zijn. Een provider kan time-outs krijgen maar je verzoek toch verwerken. Een load balancer kan een verbinding verbreken nadat de zender al opnieuw heeft geprobeerd. Of hun systeem kan even down zijn en daarna ineens een stortvloed aan oude events sturen.
Stel je een verzendpartner voor die "delivered" webhooks stuurt. Op een dag is je ontvanger 3 seconden traag, dus zij proberen opnieuw. Jij krijgt twee leveringen, je klant krijgt twee e-mails en de supportafdeling is in de war. De volgende dag hebben ze een storing en proberen ze niet opnieuw, dus "delivered" komt nooit aan en je dashboard blijft hangen.
Webhook-betrouwbaarheid gaat minder over één perfecte aanvraag en meer over ontwerpen voor rommelige realiteit: retries, idempotentie en de mogelijkheid om later te replayen en te verifiëren wat er gebeurd is.
De drie bouwstenen: retries, idempotentie, replay
Webhooks gaan twee richtingen op. Inkomende webhooks zijn calls die jij ontvangt van iemand anders (een payment provider, CRM, vervoerder). Uitgaande webhooks zijn calls die jij naar je klant of partner stuurt wanneer er iets verandert in je systeem. Beide kunnen falen om redenen die niets met jouw code te maken hebben.
Retries zijn wat er gebeurt na een fout. Een zender kan opnieuw proberen omdat hij een timeout, een 500-fout, een verbroken verbinding of geen snelle respons kreeg. Goede retries zijn verwacht gedrag, geen zeldzame randvoorwaarde. Het doel is het event door te krijgen zonder de ontvanger te overstelpen of dubbele bijwerkingen te creëren.
Idempotentie is hoe je duplicaten veilig maakt. Het betekent "doe het één keer, ook al komt het twee keer binnen". Als dezelfde webhook opnieuw arriveert, detecteer je het en geef je een succesvolle respons terug zonder de zakelijke actie nogmaals uit te voeren (bijvoorbeeld niet nog een factuur aanmaken).
Replay is je herstelknop. Het is de mogelijkheid om oude events opzettelijk, gecontroleerd, opnieuw te verwerken nadat je een bug hebt opgelost of nadat een partner een storing had. Replay verschilt van retries: retries zijn automatisch en direct, replay is doelbewust en gebeurt vaak uren of dagen later.
Als je webhook-betrouwbaarheid wilt, stel dan een paar simpele doelen en ontwerp eromheen:
- Geen verloren events (je kunt altijd vinden wat is aangekomen of wat je hebt geprobeerd te versturen)
- Veilige duplicaten (retries en replays zorgen niet voor dubbele kosten, dubbele aanmaken of dubbele e-mails)
- Duidelijk auditspoor (je kunt snel antwoord geven op "wat is er gebeurd?")
Een praktische manier om alle drie te ondersteunen is elk webhook-attempt op te slaan met een status en een unieke idempotentiesleutel. Veel teams bouwen dit als een kleine "webhook inbox/outbox"-tabel.
Inkomende webhooks: een ontvangstmethode die je kunt hergebruiken
De meeste webhook-problemen ontstaan omdat de zender en ontvanger op verschillende klokken lopen. Jouw taak als ontvanger is voorspelbaar zijn: snel erkennen, vastleggen wat binnenkwam en het veilig verwerken.
Scheid "accepteren" van "werk doen"
Begin met een flow die de HTTP-aanvraag snel houdt en het echte werk ergens anders plaatst. Dit vermindert time-outs en maakt retries veel minder pijnlijk.
- Acknowledge snel. Geef een 2xx zodra het verzoek acceptabel is.
- Check de basis. Valideer content-type, vereiste velden en parsing. Als de webhook gesigneerd is, verifieer de handtekening hier.
- Bewaar het ruwe event. Sla de body op plus de headers die je later nodig hebt (handtekening, event-ID), samen met een ontvangen-timestamp en een status zoals "received".
- Queue het werk. Maak een job voor achtergrondverwerking en geef dan je 2xx terug.
- Verwerk met duidelijke uitkomsten. Markeer het event als "processed" alleen nadat de bijwerkingen succesvol zijn. Als het faalt, noteer waarom en of het opnieuw geprobeerd moet worden.
Hoe "snel reageren" eruitziet
Een realistisch doel is onder een seconde reageren. Als de zender een specifieke code verwacht, gebruik die (veelal 200, sommige geven de voorkeur aan 202). Geef alleen 4xx terug wanneer de zender niet moet retryen (zoals een ongeldige handtekening).
Voorbeeld: een "customer.created"-webhook arriveert terwijl je database onder load staat. Met deze flow sla je nog steeds het ruwe event op, zet je het in de queue en antwoord je 2xx. Je worker kan later opnieuw proberen zonder dat de zender het opnieuw hoeft te sturen.
Inkomende veiligheidschecks die de levering niet breken
Security checks zijn de moeite waard, maar het doel is simpel: slechte traffic blokkeren zonder echte events te blokkeren. Veel leveringsproblemen ontstaan doordat ontvangers te strikt zijn of de verkeerde respons teruggeven.
Begin met het bewijzen van de zender. Geef de voorkeur aan gesigneerde requests (HMAC-handtekening header) of een gedeeld geheim token in een header. Verifieer dit voordat je zwaar werk doet en faal snel als het ontbreekt of onjuist is.
Wees voorzichtig met statuscodes omdat die retries sturen:
- Geef 401/403 voor auth-fouten zodat de zender niet eindeloos blijft retryen.
- Geef 400 voor malformed JSON of ontbrekende vereiste velden.
- Geef 5xx alleen als je service tijdelijk niet kan accepteren of verwerken.
IP-allowlists kunnen helpen, maar alleen wanneer de provider stabiele, gedocumenteerde IP-ranges heeft. Als hun IP's vaak veranderen (of ze gebruiken een groot cloud-pool), kunnen allowlists echte webhooks stilletjes laten vallen en merk je het pas veel later.
Als de provider een timestamp en een uniek event-ID meegeeft, kun je replay-bescherming toevoegen: wijs berichten af die te oud zijn en track recente ID's om duplicaten op te sporen. Houd het tijdvenster klein, maar geef een marge zodat klokverschil geen geldige requests breekt.
Een ontvanger-vriendelijke security-checklist:
- Valideer handtekening of gedeeld geheim voordat je grote payloads parsed.
- Handhaaf een maximale body-grootte en een korte request-timeout.
- Gebruik 401/403 voor auth-fouten, 400 voor malformed JSON en 2xx voor geaccepteerde events.
- Als je timestamps controleert, geef een kleine marge (bijv. een paar minuten).
Voor logging: houd een auditspoor zonder gevoelige data eeuwig te bewaren. Sla event-ID, sender-naam, ontvangsttijd, verificatieresultaat en een hash van de ruwe body op. Als je payloads moet bewaren, stel dan een retentiegrens in en mask velden zoals e-mails, tokens of betaalgegevens.
Retries die helpen, niet schaden
Retries zijn goed als ze een korte hapering veranderen in een succesvolle levering. Ze zijn schadelijk als ze traffic vermenigvuldigen, echte bugs verbergen of duplicaten creëren. Het verschil is een duidelijke regel voor wat te herhalen, hoe je pogingen spreidt en wanneer te stoppen.
Als basis, retry alleen wanneer de ontvanger waarschijnlijk later zal slagen. Een nuttig mentaal model: retry op "tijdelijke" fouten, niet op "jij stuurde iets fout".
Praktische HTTP-uitkomsten:
- Retry: netwerk-timeouts, verbindingsfouten, en HTTP 408, 429, 500, 502, 503, 504
- Niet retryen: HTTP 400, 401, 403, 404, 422
- Afhankelijk: HTTP 409 (soms "duplicate", soms een echte conflict)
Spreiding is belangrijk. Gebruik exponential backoff met jitter zodat je geen retry-storm creëert als veel events tegelijk falen. Bijvoorbeeld: wacht 5s, 15s, 45s, 2m, 5m en voeg bij elke poging een kleine willekeurige offset toe.
Stel ook een maximale retry-window en een duidelijke cutoff in. Veelvoorkomende keuzes zijn "blijf tot 24 uur proberen" of "maximaal 10 pogingen". Daarna behandel je het als een herstelprobleem, niet als een bezorgprobleem.
Om dit dagelijks werkbaar te maken, moet je eventrecord het volgende vastleggen:
- Aantal pogingen
- Laatste fout
- Volgende pogingstijd
- Eindstatus (inclusief een dead-letter-state wanneer je stopt met proberen)
Dead-letter items moeten makkelijk te inspecteren en veilig opnieuw af te spelen zijn nadat je het onderliggende probleem hebt opgelost.
Idempotentiepatronen die in de praktijk werken
Idempotentie betekent dat je hetzelfde webhook-event meerdere keren kunt verwerken zonder extra bijwerkingen te maken. Het is een van de snelste manieren om betrouwbaarheid te verbeteren, omdat retries en time-outs zullen gebeuren, zelfs als er niemand iets verkeerd doet.
Kies een sleutel die stabiel blijft
Als de provider je een event-ID geeft, gebruik die. Dat is het schoonste alternatief.
Als er geen event-ID is, bouw dan je eigen sleutel uit stabiele velden die je hebt, zoals een hash van:
- providernaam + eventtype + resource-ID + timestamp, of
- providernaam + message-ID
Sla de sleutel op plus een kleine hoeveelheid metadata (ontvangsttijd, provider, eventtype en het resultaat).
Regels die meestal standhouden:
- Behandel de sleutel als verplicht. Als je er geen kunt bouwen, quarantaineer het event in plaats van te gokken.
- Sla sleutels op met een TTL (bijv. 7 tot 30 dagen) zodat de tabel niet eeuwig groeit.
- Bewaar ook het verwerkingsresultaat (success, failed, ignored) zodat duplicaten een consistente respons krijgen.
- Zet een unieke constraint op de sleutel zodat twee parallelle requests niet allebei uitvoeren.
Maak de zakelijke actie zelf ook idempotent
Zelfs met een goede sleutel-tabel moeten je echte operaties veilig zijn. Voorbeeld: een "create order"-webhook mag niet een tweede order aanmaken als de eerste poging time-outte na de database-insert. Gebruik natuurlijke zakelijke identifiers (external_order_id, external_user_id) en upsert-patronen.
Out-of-order events komen vaak voor. Als je eerst "user_updated" krijgt en later "user_created", bepaal dan een regel zoals "pas wijzigingen alleen toe als event_version nieuwer is" of "update alleen als updated_at nieuwer is dan wat we hebben".
Duplicaten met verschillende payloads zijn het lastigst. Beslis van tevoren wat je doet:
- Als de sleutel overeenkomt maar de payload verschilt, behandel het als een provider-bug en alarmeer.
- Als de sleutel overeenkomt en de payload alleen in irrelevante velden verschilt, negeer het.
- Als je de provider niet vertrouwt, schakel dan over naar een afgeleide sleutel gebaseerd op de volledige payload-hash en behandel conflicts als nieuwe events.
Het doel is simpel: één echte verandering in de echte wereld moet één uitkomst in de echte wereld opleveren, zelfs als je het bericht drie keer ziet.
Replay-tools en auditlogs voor herstel
Als een partner-systeem onbetrouwbaar is, gaat betrouwbaarheid minder om perfecte levering en meer om snel herstel. Een replay-tool verandert "we zijn wat events verloren" in een routinefix in plaats van een crisis.
Begin met een eventlog die de levenscyclus van elk webhook-event bijhoudt: received, processed, failed of ignored. Maak het doorzoekbaar op tijd, eventtype en correlatie-ID zodat support snel kan antwoorden: "Wat is er met order 18432 gebeurd?"
Sla voor elk event genoeg context op om later dezelfde beslissing opnieuw te draaien:
- Ruwe payload en key-headers (handtekening, event-ID, timestamp)
- Genormaliseerde velden die je extraheerde
- Verwerkingsresultaat en foutmelding (indien aanwezig)
- De workflow- of mappingversie die op dat moment gebruikt werd
- Timestamps voor ontvangst, start en finish
Met dat in place voeg je een "Replay"-actie toe voor mislukte events. De knop is minder belangrijk dan de guardrails. Een goede replay-flow toont de eerdere fout, wat er op replay zal gebeuren en of het event veilig opnieuw uitgevoerd kan worden.
Guardrails die per ongeluk schade voorkomen:
- Vereis een reden-opmerking vóór replay
- Beperk replay-permissies tot een kleine rol
- Draai opnieuw door dezelfde idempotentiecontroles als bij de eerste poging
- Rate-limit replays om tijdens incidenten geen nieuwe piek te veroorzaken
- Optionele dry-run-modus die valideert zonder wijzigingen te schrijven
Incidenten omvatten vaak meer dan één event, dus ondersteun replay op tijdsbereik (bijv. "replay alle mislukte events tussen 10:05 en 10:40"). Log wie wat wanneer en waarom replayde.
Uitgaande webhooks: een verzendflow die je kunt auditen
Uitgaande webhooks falen om saaie redenen: een trage ontvanger, een korte outage, een DNS-hapering of een proxy die lange requests weggooit. Betrouwbaarheid komt door elk verzendproces te behandelen als een getraceerde, herhaalbare job, niet als een eenmalige HTTP-call.
Een verzendflow die voorspelbaar blijft
Geef elk event een stabiele, unieke event-ID. Die ID moet hetzelfde blijven bij retries, replays en zelfs service-restarts. Als je per poging een nieuwe ID genereert, maak je deduplicatie moeilijker voor de ontvanger en auditing lastiger voor jezelf.
Onderteken elk verzoek en voeg een timestamp toe. De timestamp helpt ontvangers zeer oude requests af te wijzen, en ondertekening bewijst dat de payload niet gewijzigd is tijdens transport. Houd de handtekeningregels simpel en consistent zodat partners ze zonder giswerk kunnen implementeren.
Track leveringen per endpoint, niet alleen per event. Als je hetzelfde event naar drie klanten stuurt, heeft elke bestemming zijn eigen poginggeschiedenis en eindstatus nodig.
Een praktische flow die de meeste teams kunnen implementeren:
- Maak een eventrecord met event-ID, endpoint-ID, payload-hash en initiële status.
- Verstuur de HTTP-request met een handtekening, timestamp en een idempotentiesleutel-header.
- Noteer elke poging (starttijd, eindtijd, HTTP-status, korte foutmelding).
- Retry alleen op timeouts en 5xx-responses, met exponential backoff en jitter.
- Stop na een duidelijke limiet (max pogingen of max leeftijd) en markeer het als mislukt voor review.
Die idempotentiesleutel-header is belangrijk, zelfs als jij de verzender bent. Het geeft de ontvanger een eenvoudige manier om te dedupen als zij de eerste request verwerkten maar jouw client nooit de 200-respons ontving.
Maak fouten tenslotte zichtbaar. "Failed" moet niet "verloren" betekenen. Het moet "gepauzeerd met voldoende context om veilig opnieuw te spelen" betekenen.
Voorbeeld: een wankele partner en een schoon herstel
Je support-app stuurt ticketupdates naar een partnersysteem zodat hun agenten dezelfde status zien. Elke keer dat een ticket verandert (toegewezen, prioriteit bijgewerkt, gesloten), post je een webhook-event zoals ticket.updated.
Op een middag begint de partner-endpoint te timen outen. Je eerste leveringspoging wacht, raakt je timeout en je behandelt het als "onbekend" (het kan ze bereikt hebben, het kan niet). Een goed retrybeleid probeert daarna opnieuw met backoff in plaats van elke seconde herhaalde pogingen te sturen. Het event blijft in een queue met dezelfde event-ID en elke poging wordt gelogd.
Nu het vervelende deel: als je geen idempotentie gebruikt, kan de partner duplicaten verwerken. Poging #1 heeft hen misschien bereikt, maar hun respons bereikte jou nooit. Poging #2 arriveert later en creëert een tweede "Ticket closed"-actie, stuurt twee e-mails of maakt twee timeline-items.
Met idempotentie bevat elke levering een idempotentiesleutel afgeleid van het event (vaak gewoon de event-ID). De partner bewaart die sleutel voor een periode en antwoordt "already processed" voor herhalingen. Jij stopt met gokken.
Wanneer de partner uiteindelijk terug is, is replay hoe je die ene update herstelt die echt ontbrak (bijv. een prioriteitswijziging tijdens de outage). Je pakt het event uit je auditlog en replayt het één keer, met dezelfde payload en idempotentiesleutel, zodat het veilig is, zelfs als ze het al hebben gekregen.
Tijdens het incident moeten je logs het verhaal duidelijk maken:
- Event-ID, ticket-ID, eventtype en payload-versie
- Pogingsnummer, timestamps en volgende retry-tijd
- Time-out vs non-2xx-respons vs succes
- Idempotentiesleutel verzonden en of de partner "duplicate" rapporteerde
- Een replay-record met wie het replayde en het uiteindelijke resultaat
Veelgemaakte fouten en valkuilen om te vermijden
De meeste webhook-incidenten worden niet veroorzaakt door één grote bug. Ze ontstaan door kleine keuzes die stilletjes betrouwbaarheid breken bij traffic-pieken of wanneer een derde partij haperig wordt.
De valkuilen die terugkomen in postmortems:
- Traag werk in de request-handler doen (database-writes, API-calls, file uploads) totdat de zender time-out en opnieuw probeert
- Aannemen dat providers nooit duplicaten sturen, en dan dubbel factureren, dubbele orders aanmaken of twee e-mails sturen
- De verkeerde statuscodes teruggeven (200 zelfs als je het event niet accepteerde, of 500 voor slechte data die nooit zal slagen bij retry)
- Shippen zonder correlatie-ID, event-ID of request-ID en dan uren besteden aan logs matchen met klantmeldingen
- Oneindig blijven retryen, wat een backlog opbouwt en van een partner-outage je eigen outage maakt
Een eenvoudige regel houdt stand: acknowledge snel, verwerk veilig. Valideer alleen wat je nodig hebt om te beslissen of je het event accepteert, sla het op en doe de rest asynchroon.
Statuscodes zijn belangrijker dan men verwacht:
- Gebruik 2xx alleen als je het event hebt opgeslagen (of in de queue gezet) en je er vertrouwen in hebt dat het verwerkt wordt.
- Gebruik 4xx voor ongeldige input of mislukte auth zodat de zender stopt met retryen.
- Gebruik 5xx alleen voor tijdelijke problemen aan jouw kant.
Stel een retry-plafond in. Stop na een vaste window (bijv. 24 uur) of een vast aantal pogingen, en markeer het event als "needs review" zodat iemand kan beslissen wat te replayen.
Snel checklist en vervolgstappen
Webhook-betrouwbaarheid draait vooral om herhaalbare gewoontes: accepteer snel, dedupe agressief, retry met beleid en houd een replay-pad.
Inkomend (ontvanger) korte checks
- Geef snel een 2xx terug zodra het verzoek veilig is opgeslagen (doe langzaam werk async).
- Sla genoeg van het event op om te bewijzen wat je ontving (en later te debuggen).
- Vereis een idempotentiesleutel (of leid er één af van provider + event-ID) en handhaaf die in de database.
- Gebruik 4xx voor slechte handtekening of ongeldig schema en 5xx alleen voor echte serverproblemen.
- Volg verwerkingsstatus (received, processed, failed) plus de laatste foutmelding.
Uitgaand (verzender) korte checks
- Wijs per event een unieke event-ID toe en houd die stabiel over pogingen.
- Onderteken elk verzoek en voeg een timestamp toe.
- Definieer een retrybeleid (backoff, max pogingen en wanneer te stoppen) en houd je eraan.
- Houd per-endpoint status bij: laatste succes, laatste fout, opeenvolgende fouten, volgende retry-tijd.
- Log elke poging met genoeg detail voor support en audits.
Voor ops: beslis van tevoren wat je zult replayen (single event, batch op tijdsbereik/status of beide), wie het mag doen en wat je dead-letter-reviewroutine is.
Als je deze onderdelen wilt bouwen zonder alles handmatig te koppelen, kan een no-code platform zoals AppMaster (appmaster.io) praktisch zijn: je kunt webhook inbox/outbox-tabellen in PostgreSQL modelleren, retry- en replayflows ontwerpen in een visuele Business Process Editor en een intern adminpaneel leveren om mislukte events te zoeken en opnieuw uit te voeren als partners haperen.
FAQ
Webhooks staan tussen systemen die je niet onder controle hebt, dus je erft hun timeouts, outages, retries en schemawijzigingen. Zelfs als jouw code correct is, zie je duplicaten, missende events, vertragingen en uit-of-volgorde leveringen.
Ontwerp vanaf dag één voor retries en duplicaten. Sla elk binnenkomend event op, antwoord snel met een 2xx zodra het veilig is vastgelegd, en verwerk het asynchroon met een idempotentiesleutel zodat herhaalde leveringen geen dubbele bijwerkingen veroorzaken.
Je moet snel erkennen na basisvalidatie en opslag, meestal binnen een seconde. Als je traag werk in de request doet, time-outs en retries nemen toe, wat duplicaten en onduidelijkheid bij incidenten veroorzaakt.
Zie idempotentie als: “voer de zakelijke actie één keer uit, zelfs als het bericht meerdere keren aankomt.” Je handhaaft het door een stabiele idempotentiesleutel te gebruiken (vaak de provider's event-ID), deze op te slaan en voor duplicaten succes terug te geven zonder de actie opnieuw uit te voeren.
Gebruik de event-ID van de provider als die er is. Als die er niet is, leid dan een sleutel af uit stabiele velden die je vertrouwt en vermijd velden die tussen retries kunnen veranderen. Als je geen stabiele sleutel kunt maken, quarantaineer het event voor review in plaats van te gokken.
Geef 4xx terug voor problemen die de verzender niet kan oplossen door te retryen, zoals mislukte authenticatie of een ongeldig payload. Gebruik 5xx alleen voor tijdelijke problemen aan jouw kant. Wees consistent, want de statuscode bepaalt vaak of de verzender opnieuw probeert.
Retry op timeouts, verbindingsfouten en tijdelijke serverreacties zoals 408, 429 en 5xx. Gebruik exponential backoff met jitter en een duidelijke stopregel, zoals een maximaal aantal pogingen of maximale leeftijd, en zet het event daarna in een “needs review”-status.
Replay is het doelbewust opnieuw verwerken van oude events nadat je een bug hebt opgelost of uit een outage bent hersteld. Retries zijn automatisch en direct. Een goede replay heeft een eventlog, veilige idempotentiecontroles en beschermingsmaatregelen zodat je niet per ongeluk werk dupliceert.
Ga ervan uit dat events uit de volgorde kunnen komen en kies een regel die bij je domein past. Een veelgebruikte aanpak is updates alleen toepassen als een event-versie of timestamp nieuwer is dan wat je al hebt opgeslagen, zodat late aankomsten de huidige staat niet overschrijven.
Bouw een eenvoudige webhook inbox/outbox-tabel en een klein adminoverzicht om mislukte events te zoeken, inspecteren en opnieuw af te spelen. In AppMaster (appmaster.io) kun je deze tabellen in PostgreSQL modelleren, dedupe-, retry- en replayflows in de Business Process Editor zetten en een intern paneel leveren zonder alles handmatig te coderen.


