Stripe webhooks idempotency: best practice
I webhook Stripe possono arrivare più volte per lo stesso evento (per via di retry su timeout o per la delivery at-least-once). Senza idempotency rischi di processare lo stesso evento duplicato, con conseguenze gravi: ordini duplicati, doppi addebiti, email ripetute.
Pattern idempotency standard
Il pattern classico salva ogni event.id ricevuto in una tabella dedicata (es. processed_webhook_events con UNIQUE constraint su event_id). All'arrivo del webhook, prima di tutto inserisci event_id; se l'INSERT fallisce per duplicate key, l'evento è già stato processato e rispondi 200 senza fare nulla. Questo richiede che il processing avvenga in una transazione atomica con l'INSERT idempotency.
Idempotency a livello business
Oltre all'idempotency a livello evento Stripe, considera quella a livello business: se ricevi payment_intent.succeeded per il PaymentIntent X due volte, verifica se l'ordine corrispondente è già 'paid' nel tuo DB prima di duplicare azioni (invio email, fulfillment). Combinare entrambi gli approcci garantisce robustezza assoluta.
Patterns per processing pesanti: queue async
Per webhook che richiedono processing pesante (sync con CRM, invio email, attivazione provisioning servizio), best practice: 1) Webhook handler salva event_id + payload in DB e risponde 200 immediatamente (< 1s); 2) Worker async (Sidekiq, Celery, Bull, Resque) processa l'evento; 3) Idempotency anche a livello worker per evitare retry duplication. Vantaggi: timeout Stripe evitato, scalabilità' orizzontale, retry logic separato dal delivery webhook. Esempio: Stripe webhook -> insert SQS message -> Lambda processes async. Pattern usato da Notion, Linear, Slack.
Testing idempotency e chaos engineering
Test idempotency con scenari: 1) Stesso event ricevuto 2 volte (Stripe CLI: stripe trigger 2 volte stesso evento); 2) Webhook ricevuto out-of-order (es. invoice.payment_failed prima di invoice.created, possibile per retry); 3) Webhook ricevuto dopo cleanup row (oltre 90 giorni); 4) Webhook con event.data.object obsoleto (Stripe envia ancora il vecchio object in retry). Test che ogni scenario non produca duplicati, stato inconsistente o errori 500. Stripe-CLI integration in CI/CD pipeline esegue trigger automatici per regression test.
Idempotency con database transaction
Pattern transactional robusto in SQL: BEGIN; INSERT INTO webhook_events (event_id, received_at, status) VALUES ($1, NOW(), 'processing') ON CONFLICT DO NOTHING; SELECT count(*) -> if 0, evento already processed - return 200. Else process business logic (UPDATE order SET status='paid'; INSERT INTO email_queue...); UPDATE webhook_events SET status='completed' WHERE event_id=$1; COMMIT. Garanzia: either everything succeeds or nothing changes (rollback on error). Index obbligatorio su event_id (UNIQUE constraint). Per high-throughput, considera PostgreSQL ON CONFLICT performance benefits vs check-then-insert pattern.
Race condition e distributed system
Webhook handler in distributed system (multiple replica behind load balancer) può avere race condition: stesso event delivered a 2 replica simultaneously. Senza database lock, entrambe processano. Soluzione: INSERT con UNIQUE constraint atomico - solo una replica winning, l'altra fallisce e skip. Alternative: distributed lock via Redis/etcd con TTL (se uno crasha, lock auto-released). Test scenario con chaos engineering: kill replica during processing per verificare retry coverage. Goal: at-least-once delivery + idempotency = effectively-once business outcome.
Webhook security e zero-trust
Webhook è entry point pubblico - applica zero-trust: 1) Verifica firma Stripe (non opzionale); 2) Rate limiting (proteggi da DoS); 3) Request size limit (max 1MB tipicamente); 4) Logging completo per audit; 5) Alerting su pattern anomali (volume spike, invalid signature spike); 6) Network segregation - webhook endpoint isolato da database admin via firewall rule. Per business regulated (finance, healthcare), considera anche: mutual TLS (mTLS) configurabile in Stripe Dashboard per double authentication, IP whitelist Stripe known ranges (con caveat: range cambia, manutenzione). Compliance audit pass: documenta webhook architecture in security policy aziendale.
Webhook reliability patterns avanzati
Pattern avanzati reliability: 1) Webhook saga - sequenza eventi correlati (subscription.created -> invoice.created -> invoice.paid) processed in ordine; 2) Outbox pattern - webhook scrive evento in DB outbox, separate process publishes to downstream; 3) Event sourcing - tutti eventi Stripe stored come append-only log, business state derivato; 4) Idempotency tier - lock at event level + lock at business operation level; 5) Dead letter queue per webhook che falliscono ripetutamente, alerting team operations. Per business mission-critical (fintech, healthcare, regulated), questi pattern sono standard. Investment in reliability webhook = sleep better at night.
Procedura passo-passo
- Crea tabella processed_webhook_events con colonna event_id UNIQUE.
- All'arrivo del webhook, START TRANSACTION.
- Esegui INSERT INTO processed_webhook_events (event_id) VALUES (?).
- Se INSERT fallisce per duplicate key, COMMIT e return 200.
- Altrimenti, esegui il processing business (update ordine, send email, ecc.).
- Tutto dentro la stessa transazione: rollback su errore.
- COMMIT alla fine del processing.
- Aggiungi cleanup periodico (cron) per eliminare event_id vecchi (>90 giorni).
Errori comuni e come risolverli
- Idempotency check senza transazione: race condition tra due webhook concorrenti.
- Solo idempotency su event_id senza business check: se il flusso ha race condition non basta.
- Tabella non indicizzata: lookup lento; UNIQUE crea automaticamente index.
- Cleanup mai eseguito: tabella cresce all'infinito; cleanup mensile.
Domande frequenti
D: Quanto spesso Stripe duplica i webhook?
R: Raro, ma capita su timeout/network issue; statisticamente <1%.
D: Posso usare una tabella in memoria (Redis)?
R: Sì, ma assicurati persistenza durante restart.
D: Idempotency va anche su webhook critici?
R: Sì, soprattutto su quelli; payment_intent.succeeded è il classico.
D: Cosa succede se il processing fallisce?
R: Return 500 e Stripe ritenta; idempotency garantisce no doppi side-effect.
Hai bisogno di aiuto?
Se vuoi integrare Stripe con il team di G Tech Group, scrivici tramite il modulo di contatto.