# Consegna e affidabilità dei webhook: retry, ordinamento, dead-letter

> Un webhook è una callback HTTP server-to-server: la piattaforma di messaggistica esegue un POST di un evento verso l'URL del consumer. La consegna affidabile è at-least-once, quindi il producer ritenta le consegne fallite con backoff esponenziale e jitter, esegue il dead-letter dopo un limite, e il consumer deve essere idempotente perché duplicati ed eventi fuori ordine sono attesi.

- URL canonico: https://www.broadnet.me/it/reference/webhook-delivery-and-reliability
- Lingua: it
- Ultimo aggiornamento: 2026-07-19
- Editore: BroadNet (https://www.broadnet.me)

L'architettura standard dei webhook/callback e il suo modello di affidabilità: consegna at-least-once, backoff esponenziale con jitter sui retry, timeout di consegna, garanzie di ordinamento, dead-letter queue, consumer idempotenti e salute dell'endpoint / circuit-breaking per proteggere un ricevitore in difficoltà.

## Architettura dei webhook

Un webhook inverte i consueti ruoli client/server: invece che l'applicazione faccia polling della piattaforma di messaggistica per gli aggiornamenti, è la piattaforma a eseguire un POST di un evento verso un URL che l'applicazione ha registrato — un cambio di stato di consegna, un messaggio in ingresso, un evento di account. Questa è la metà push del modello asincrono la cui metà pull è il polling su GET /messages/{id}. L'endpoint del consumer è un normale server HTTPS che deve accettare un POST, fare il minimo necessario per registrare in modo durevole l'evento e restituire rapidamente un 2xx. Un pattern comune e importante è confermare alla ricezione ed elaborare in modo asincrono: l'endpoint scrive l'evento grezzo su una coda o su un log, restituisce 200, e un worker separato svolge il lavoro effettivo — così una business logic lenta non causa mai un timeout e un retry del producer.

*Un POST di webhook di stato di consegna verso il consumer*

```http
POST /webhooks/dlr HTTP/1.1
Host: app.example
Content-Type: application/json
Webhook-Id: 8b1a...
Webhook-Timestamp: 1752916522
Webhook-Signature: v1,3f2b...

{
  "event": "message.delivered",
  "message_id": "01HZY8Q3M6",
  "status": "delivered",
  "occurred_at": "2026-07-19T09:15:25Z"
}
```

## Garanzie di consegna: at-least-once

La consegna distribuita non può essere contemporaneamente exactly-once e disponibile, quindi quasi ogni sistema di webhook in produzione sceglie at-least-once: il producer continua a ritentare finché non vede un acknowledgement di successo, accettando che un consumer possa occasionalmente ricevere lo stesso evento più di una volta. Gli estremi alternativi sono peggiori — at-most-once perde eventi al primo fallimento, e un vero exactly-once richiede un coordinamento distribuito che il transport HTTP non fornisce. At-least-once impone un obbligo al consumer: deve essere idempotente, deduplicando su un identificatore di evento stabile così che un evento riconsegnato sia innocuo. Il contratto di acknowledgement è semplice: qualsiasi 2xx significa 'smetti di ritentare'; qualsiasi altra cosa (o un timeout) significa 'ritenta più tardi'.

## Retry con backoff esponenziale e jitter

Quando una consegna fallisce o va in timeout, il producer ritenta secondo una pianificazione che cresce in modo esponenziale — un ritardo base raddoppiato a ogni tentativo fino a un tetto — così che un endpoint brevemente non disponibile non venga martellato. Fondamentale, il ritardo include un jitter casuale: senza di esso, una flotta di eventi tutti falliti durante la stessa interruzione ritenterebbe all'unisono, producendo un thundering herd sincronizzato nel momento in cui l'endpoint si riprende. Il full jitter (un valore casuale tra zero e il limite di backoff corrente) distribuisce quel carico in modo uniforme. I retry proseguono finché l'evento non riesce o non si raggiunge un limite totale di tentativi o di tempo, dopo il quale l'evento viene messo in dead-letter invece di essere ritentato all'infinito.

*Backoff esponenziale con full jitter*

```text
base = 2s ; factor = 2 ; cap = 3600s
attempt n bound = min(cap, base * factor^(n-1))
delay(n)        = random(0, bound)      # full jitter

n=1  bound  2s    delay in [0, 2s]
n=2  bound  4s    delay in [0, 4s]
n=3  bound  8s    delay in [0, 8s]
n=4  bound 16s    delay in [0, 16s]
...  until success or attempt/time cap -> dead-letter
```

## Timeout e ordinamento

Un producer imposta un timeout per consegna breve — comunemente pochi secondi — e tratta un timeout allo stesso modo di un fallimento: la connessione viene abbandonata e l'evento viene pianificato per il retry. Questa è la ragione meccanica per cui i consumer dovrebbero confermare rapidamente ed elaborare in modo asincrono. L'ordinamento è la garanzia più difficile. Poiché retry e consegne parallele si intrecciano, uno stream di webhook naïf è ordinato solo best-effort: un evento precedente ritentato può arrivare dopo uno successivo. I sistemi che necessitano di ordine o consegnano per chiave in sequenza (tutti gli eventi per un id di messaggio su un'unica partizione ordinata, con blocco in caso di fallimento) oppure, più comunemente, rendono il payload auto-ordinante — ogni evento trasporta un timestamp e una sequence monotona così che il consumer possa scartare un aggiornamento obsoleto invece di dipendere dall'ordine di arrivo.

*Strategie di ordinamento per gli stream di webhook*

| Strategia | Come | Costo |
|---|---|---|
| Best-effort | Consegnare non appena disponibile, riprovare in modo indipendente | Gli eventi possono verificarsi in ordine sparso |
| Ordinato per chiave | Serializza tutti gli eventi per un singolo ID; blocca in caso di errore | Blocco head-of-line, throughput inferiore |
| Payload auto-ordinante | Trasporta timestamp + sequenza; il consumer scarta i dati obsoleti | Logica del consumer, ma nessun blocco |

## Dead-letter e salute dell'endpoint

Un endpoint che resta giù non deve consumare capacità di retry all'infinito. Una volta esaurita la retry policy, l'evento viene spostato in una dead-letter queue — uno store durevole di eventi non consegnabili che un operatore può ispezionare e riprodurre una volta corretto l'endpoint — invece di essere scartato silenziosamente. Per proteggere entrambe le parti mentre un endpoint è in cattiva salute, i producer monitorano la salute di consegna per endpoint e applicano il circuit-breaking: dopo una serie prolungata di fallimenti il circuito si apre e i nuovi eventi vengono parcheggiati o fatti fallire rapidamente invece di essere tentati, poi una sonda half-open testa periodicamente il recupero prima che la consegna normale riprenda. Questo impedisce a un consumer morto di generare un backlog di tentativi condannati e permette a uno in recupero di riprendersi con gradualità. Il comportamento concreto di webhook e retry di BroadNet è documentato per gli sviluppatori.

## Costruire un consumer idempotente

Poiché la consegna at-least-once garantisce duplicati, è il consumer — non il producer — a possedere gli effetti exactly-once. Il pattern è uno store di dedup con chiave su un identificatore di evento stabile fornito dal producer (un Webhook-Id o event id, mai il solo hash del payload). Alla ricezione il consumer verifica se ha già elaborato quell'id; in caso affermativo conferma con 2xx e non fa nient'altro; in caso contrario registra l'id e applica l'effetto nella stessa transazione, così che un crash non possa applicare l'effetto senza registrare l'id. Combinato con payload auto-ordinanti, questo rende un evento riconsegnato o fuori ordine un no-op, ed è ciò che trasforma un transport at-least-once in esiti di business exactly-once.

*Consumer idempotente (pseudocodice)*

```text
on POST(event):
  verify_signature(event)          # reject if invalid
  id = event.headers['Webhook-Id']
  begin transaction:
    if seen(id): return 200        # duplicate -> no-op
    if event.sequence <= last_seq(event.message_id):
        mark_seen(id); return 200  # stale -> discard
    apply_effect(event)
    mark_seen(id); set_last_seq(...)
  commit
  return 200
```

## Domande frequenti

### Quale garanzia di consegna forniscono i webhook?

At-least-once. Il producer ritenta finché non vede un acknowledgement 2xx, quindi un consumer può ricevere lo stesso evento più di una volta. Exactly-once su HTTP puro richiederebbe un coordinamento distribuito che il transport non fornisce, quindi ci si aspetta che il consumer sia idempotente e deduplichi su un event id stabile.

### Perché aggiungere jitter al backoff di retry dei webhook?

Senza jitter, ogni evento fallito durante una singola interruzione ritenta all'unisono quando l'endpoint si riprende, creando un thundering herd sincronizzato che può metterlo di nuovo giù. Il full jitter — un ritardo casuale tra zero e il limite di backoff corrente — distribuisce i retry in modo uniforme così che l'endpoint in recupero veda una salita graduale.

### Come dovrebbe rispondere un endpoint di webhook?

Rapidamente e con un 2xx. Il pattern consigliato è registrare in modo durevole l'evento grezzo, restituire immediatamente 200 ed elaborare in modo asincrono in un worker separato. In questo modo una business logic lenta non fa mai scattare il timeout di consegna del producer né innesca un retry inutile.

### Gli eventi di webhook vengono consegnati in ordine?

Per impostazione predefinita solo best-effort — retry e consegne parallele possono intrecciarsi, quindi un evento precedente può arrivare dopo uno successivo. Per ottenere l'ordine, o si serializza per chiave (al costo dell'head-of-line blocking) oppure si rende il payload auto-ordinante con un timestamp e una sequence così che il consumer scarti gli aggiornamenti obsoleti.

### Che cos'è una dead-letter queue per i webhook?

Uno store durevole per gli eventi che non è stato possibile consegnare dopo l'esaurimento della retry policy. Invece di scartarli silenziosamente, il producer li parcheggia così che un operatore possa ispezionare i fallimenti e riprodurre gli eventi una volta corretto l'endpoint del consumer.

### Perché applicare il circuit-breaking agli endpoint di webhook?

Un endpoint che fallisce in modo persistente consumerebbe altrimenti capacità di retry con tentativi condannati e accumulerebbe un backlog. Il circuit-breaking si apre dopo fallimenti prolungati, parcheggiando o facendo fallire rapidamente i nuovi eventi, poi usa una sonda half-open per testare il recupero prima di riprendere la consegna normale — proteggendo sia il producer sia il consumer in recupero.

## Fonti

- [IETF RFC 9110 — HTTP Semantics (method properties, 202)](https://datatracker.ietf.org/doc/html/rfc9110)
- [IETF draft — Idempotency-Key HTTP Header Field](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header)

## Correlati

- [Verifica della firma dei webhook](https://www.broadnet.me/it/reference/webhook-signature-verification)
- [Webhook di stato e inbound dei messaggi](https://www.broadnet.me/it/reference/message-status-and-inbound-webhooks)
- [Idempotenza e rate limiting](https://www.broadnet.me/it/reference/idempotency-and-rate-limiting)
- [Guida ai webhook di BroadNet](https://www.broadnet.me/it/developers/webhooks)
- [Retry e backoff di BroadNet](https://www.broadnet.me/developers/retries)
