Webhooks : répétitions et accrochage
1) Modèle de livraison de base
At-least-once (par défaut) : l'événement sera livré ≥1 fois. Les garanties sont assurées exactement ou une fois par l'idempotence du récepteur.
Clavardage (ACK) : n'importe quel 2xx (habituellement 200/204) du destinataire signifie succès. Tout le reste est interprété comme un refus et conduit à une répétition.
ACK rapide : Répondez 2xx après avoir placé l'événement à son tour, plutôt qu'après un traitement complet.
2) Format des événements et titres obligatoires
Charge utile (exemple)
json
{
"id": "evt_01HXYZ",
"type": "order. created",
"occurred_at": "2025-11-03T18:10:12Z",
"sequence": 128374,
"source": "orders",
"data": { "order_id": "o_123", "amount": "49. 90", "currency": "EUR" },
"schema_version": 1
}
Titres de l'expéditeur
"X-Webhook-ID : evt_01HXYZ' - ID d'événement unique (utiliser pour la déduplication).
« X-Webhook-Seq : 128374 » est une séquence monotone (par abonnement/thème).
`X-Signature: sha256=<base64(hmac_sha256(body, secret))>` — HMAC-подпись.
« X-Retry : 0,1,2... » est le numéro de la tentative.
« X-Webhook-Version : 1 » est la version du contrat.
(en option) « Traceparent » est une corrélation de pistes.
Réponse du destinataire
2xx - accepté avec succès (il n'y aura plus de répétitions sur ce 'id').
410 Gone - endpoint est supprimé/inactif → l'expéditeur arrête les répétitions et désabonne.
429/5xx/timaut - l'expéditeur répète sur la politique des retraits.
3) Politique de répétition (retries)
Escalier backoff recommandé (+ jitter)
« 1s, 3s, 10s, 30s, 2m, 10m, 30m, 2h, 6h, 24h » (on s'arrête après une limite, par exemple de 48 à 72 heures).
Règles :- Backoff exponentiel + jitter aléatoire (± 20-30 %) pour éviter l'effet « troupeau ».
- Quorum d'erreur pour les pannes temporelles (par exemple, répétition si 5xx ou temporisation du réseau).
- Respect 429 : mettez au minimum 'min (titre Retry-After, fenêtre backoff suivante)'.
Délais et tailles
Délai de connexion ≤ 3-5 secondes ; le délai total de réponse est ≤ 10 secondes.
Taille du corps contractuel (par exemple, ≤ 256 Ko), sinon 413 → logique « chunking » ou « pull URL ».
4) Idempotence et déduplication
Application idempotente : le traitement des répétitions du même 'id'doit renvoyer le même résultat et ne pas changer l'état à nouveau.
Stockage dedup sur le côté destinataire : Stocker '(X-Webhook-Id, processed_at, checksum)' avec la TTL ≥ la fenêtre des retraits (24-72 h).
Clé de composition : si plusieurs tops → '(subscription_id, event_id)'.
5) Ordre et « effets exactly-once »
Il est difficile de garantir un ordre strict dans les systèmes distribués. Utilisez :- Partition by key : le même ensemble logique (par exemple, 'order _ id') est toujours dans le même « canal » de livraison.
- Séquence : Rejetez les événements avec l'ancien 'X-Webhook-Seq' et mettez-les dans le « lot de stationnement » avant l'arrivée des manquants.
- journal des opérations appliquées (outbox/inbox pattern),
- transactionnel upsert par 'event _ id' dans la base de données,
- saga/compensation pour les processus complexes.
6) Résolution des erreurs de code de statut (tableau)
7) Sécurité du canal
La signature HMAC de chaque message ; la vérification sur le récepteur avec la « fenêtre temporelle » (attaques mitm et replay).
mTLS pour les domaines sensibles (CUS/paiements).
IP allowlist des adresses sortantes, TLS 1. 2+, HSTS.
PII-minimisation : n'envoyez pas de données personnelles supplémentaires ; masquer dans les loges.
Rotation des secrets : deux clés actives (active/next) et l'en-tête "X-Key-Id'pour indiquer le courant.
8) Files d'attente, DLQ et relais
Les événements sont obligatoirement écrits dans la file d'attente/journal de sortie du côté de l'expéditeur (pour un relais fiable).
Si vous dépassez le maximum de rétroactifs - l'événement passe au DLQ (Dead Letter Queue) avec la raison.
API Replay (pour le destinataire/opérateur) : retransmission par 'id '/plage temporelle/sujet, avec restriction RPS et signature/autorisation supplémentaire.
POST /v1/webhooks/replay
{ "subscription_id": "sub_123", "from": "2025-11-03T00:00:00Z", "to": "2025-11-03T12:00:00Z" }
→ 202 Accepted
9) Contrat et version
Versez l'événement (« schema _ version ») et le transport (« X-Webhook-Version »).
Ajouter les champs uniquement en option ; lors de la suppression, la migration mineure et la période de transition (dual-write).
Documentez les types d'événements, les exemples, les schémas (JSON Schema), les codes d'erreur.
10) Observabilité et SLO
Mesures clés de l'expéditeur :- 'delivery _ success _ rate' (2xx/tous les essais), 'first _ attempt _ success _ rate'
- `retries_total`, `max_retry_age_seconds`, `dlq_count`
- `latency_p50/p95` (occurred_at → ack_received_at)
- `ack_latency` (receive → 2xx), `processing_latency` (enqueue → done)
- `duplicates_total`, `invalid_signature_total`, `out_of_order_total`
99. 9 % des événements reçoivent le premier ACK ≤ 60 secondes (28d).
- DLQ ≤ 0. 1 % du total ; repli DLQ ≤ 24 h.
11) Temporisation et ruptures de réseau
Utilisez UTC dans les champs de temps ; Synchroniser NTP.
Envoyez 'occurred _ at' et fixez 'delivered _ at' pour compter lag.
En cas de rupture prolongée du réseau/endpoint, accumulez les → dans la file d'attente, limitez la croissance (backpressure + quotas).
12) Limites recommandées et hygiène
RPS d'abonnement (par example 50 RPS, burst 100) + parallélisme (par example 10).
Max. corps : 64-256 Ko ; Pour plus, « notification + URL » et signature de téléchargement.
Noms des événements dans 'snake. case 'ou' dot. type` (`order. created`).
L'idempotence stricte des opérations write du récepteur.
13) Exemples : Expéditeur et destinataire
13. 1 Expéditeur (pseudo-code)
python def send_event(event, attempt=0):
body = json. dumps(event)
sig = hmac_sha256_base64(body, secret)
headers = {
"X-Webhook-Id": event["id"],
"X-Webhook-Seq": str(event["sequence"]),
"X-Retry": str(attempt),
"X-Signature": f"sha256={sig}",
"Content-Type": "application/json"
}
res = http. post(endpoint, body, headers, timeout=10)
if 200 <= res. status < 300:
mark_delivered(event["id"])
elif res. status == 410:
deactivate_subscription()
else:
schedule_retry(event, attempt+1) # backoff + jitter, respect 429 Retry-After
13. 2 Destinataire (pseudo-code)
python
@app. post("/webhooks")
def handle():
body = request. data headers = request. headers assert verify_hmac(body, headers["X-Signature"], secret)
evt_id = headers["X-Webhook-Id"]
if dedup_store. exists(evt_id):
return, "" 204 enqueue_for_processing (body) # fast path. dedup_store put(evt_id, ttl=723600)
return, "" 202 # or 204
14) Tests et pratiques de chaos
Cas négatifs : signature non valide, 429/5xx, temporisation, 410, gros payload's.
Comportemental : out-of-order, doublons, retards de 1 à 10 minutes, écart de 24 heures.
Charge : burst 10 × ; vérifier la résilience et la résilience du DLQ.
Contrats : JSON Schema, titres obligatoires, types d'événements stables.
15) Chèque de mise en œuvre
- 2xx = ACK, et retour rapide après l'enquête
- Exponentielle backoff + jitter, respect de 'Retry-After'
- Idempotence du récepteur et du dedup selon "X-Webhook-Id' (TTL ≥ retrai)
- Signatures HMAC, rotation des secrets, optional mTLS
- API DLQ + Replay, surveillance et alertes
- Restrictions : Délai, RPS, taille du corps
- Ordre : partition par clé ou « sequence » + « parking lot »
- Documentation : schémas, exemples, codes d'erreur, versions
- Tests de chaos : retards, prises, défaillance du réseau, replay prolongé
16) Mini-FAQ
Dois-je toujours répondre 200 ?
N'importe quel 2xx est considéré comme un succès. 202/204 est une pratique normale pour la « file d'attente ».
Peut-on arrêter les répétitions ?
Oui, par la réponse 410 et/ou via la console/API de l'expéditeur (désactivation de l'abonnement).
Qu'en est-il des grands payload'ami ?
Envoyez une « notification + URL sécurisée », signez votre demande de téléchargement et installez TTL.
Comment assurer l'ordre ?
Partition by key + `sequence`; en cas de divergence - « parking lot » et rejouer.
Résultat
Les webhooks fiables sont une sémantique ACK (2xx) claire, des répétitions intelligentes avec backoff + jitter, une idempotence et une déduplication strictes, une sécurité compétente (HMAC/mTLS), une file d'attente + DLQ + et une observabilité transparente. Fixez le contrat, entrez les limites et les métriques, chassez régulièrement les scénarios de chaos - et vos intégrations cesseront d'éclater au premier crash.