2026-05-20
Webhook Stripe: idempotenza, duplicati e concorrenza
Schema minimo e handler transazionale per eventi duplicati, out-of-order e at-least-once delivery.
Stripe (e analoghi seri) consegna webhook at-least-once: lo stesso id evento può arrivare due volte, e due eventi diversi possono arrivare in ordine invertito rispetto alla timeline business. Il tuo handler deve essere idempotente e serializzabile rispetto alla chiave naturale dell’evento.
Documentazione di riferimento: Gestire eventi duplicati e verifica firma con signing secret (header Stripe-Signature). La versione API (Stripe-Version) va allineata al tuo backend.
Semantica at-least-once
Implica tre obblighi:
- Rispondere 2xx rapidamente dopo aver persistito l’evento o averlo accodato in modo transazionale—Stripe ritenta su timeout o 5xx.
- Non applicare side-effect duplicati se lo stesso
event.idviene ritentato. - Ordinare gli update di stato business con cautela:
customer.subscription.updatedpuò arrivare dopo…deletedin caso di retry—usa versione monotona o confronto timestamp Stripe (event.created) con locking.
Schema tabella minimo
CREATE TABLE stripe_events (
id text PRIMARY KEY, -- evt_*
type text NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
processed_at timestamptz,
payload_hash bytea
);
id è l’id Stripe dell’evento, non il tuo id interno. processed_at null = da processare o in flight.
Handler transazionale (pseudocodice)
BEGIN;
INSERT INTO stripe_events (id, type) VALUES ($eventId, $type)
ON CONFLICT (id) DO NOTHING;
IF NOT FOUND (row inserted) THEN
COMMIT; RETURN 200;
END IF;
INSERT INTO jobs (event_id, ...) VALUES ($eventId, ...);
COMMIT;
Se la logica di business è lunga, pattern sicuro: transazione breve che deduplica + accoda; worker separato che marca processed_at.
Concorrenza e unique constraint
Due istanze dello stesso handler possono ricevere lo stesso POST quasi in parallelo. UNIQUE(event.id) + INSERT … ON CONFLICT (Postgres) o equivalente in SQLite (INSERT OR IGNORE) evita doppie elaborazioni. Evita “SELECT then INSERT” senza lock—race garantita.
Sicurezza e replay
Prima di toccare DB:
- Verifica firma con secret del endpoint webhook (non la secret API generica).
- Controlla
timestampnel payload firma contro clock skew (Stripe documenta tolleranza default). - Ignora eventi troppo vecchi se il dominio lo richiede (finestra anti-replay).
Test minimi
- Doppio POST identico → una sola riga
stripe_eventscon side-effect una tantum. - Due eventi sequenziali invertiti simulati (mock) → stato finale coerente con regola scelta (es. max
createdvince). - Timeout: handler lento > soglia → Stripe ritenta; verifica che il secondo passaggio non raddoppi addebiti interni.
Cosa dice Stripe (verificabile)
- Retry: documentazione e prodotti webhook ritentano le consegne fallite per fino a ~3 giorni con backoff—tratta ogni
event.idcome idempotency key naturale. Approfondimento operativo: Process undelivered events e panorama Receive Stripe events. constructEvent/ verifica firma: le librerie ufficiali richiedono il corpo grezzo (raw body) del POST + headerStripe-Signature+ webhook secret dell’endpoint—middleware che parsano JSON prima della verifica rompono la firma. Riferimento: Resolve signature errors.- CLI locale:
stripe listen --forward-to localhost:3000/webhookresta il modo più rapido per catturare payload reali quando integri per la prima volta.
Limitazioni
Non copre Stripe Connect multi-account, thin events beta, né idempotency key sulle API di pagamento (argomento affine ma distinto).
Sintesi
Persisti l’id evento con vincolo unico prima di side-effect; rispondi 2xx dopo commit; sposta business lento in coda. Così sopravvivi a duplicati, retry e una dose di out-of-order senza perdita di denaro o stato.