Stripe envía millones de eventos de pago en tiempo real, pero su fiabilidad depende enteramente del backend que procesa sus webhooks. Si el sistema no protege contra duplicados, caídas temporales o reenvíos, los riesgos se traducen en doble facturación, procesos de negocio inconsistentes y pérdidas directas.
Diseñar pipelines idempotentes y robustos, especialmente en arquitecturas serverless —donde cold starts y timeouts no se pueden evitar— exige controlar cada detalle: desde colas a nivel de integración hasta upserts transaccionales en la base de datos. Aquí entra nuestra experiencia: ¿cómo bloquear duplicados, garantizar entrega y no perder eventos, usando Node.js, FastAPI, SQS y PostgreSQL en AWS?
Patrones de fallo y el reto del reenvío en Stripe
Para ingenieros: Anatomía real del doble evento
- Stripe reintenta cada evento hasta 72h con backoff exponencial si recibe un HTTP != 2xx o timeout.
- En serverless, las lambdas pueden cortarse entre commits. Eventos "fantasma" aparecen si la confirmación HTTP va antes que el upsert DB.
- Paralelismo: Si Stripe reenvía y tu endpoint escala, varios workers ven el mismo "payment_intent.succeeded" casi a la vez (race condition).
Stripe documenta "At least once delivery". No garantiza orden ni única entrega. Cualquier arquitectura operativa requiere deduplicar al menos en el backend, no sólo en lógica de negocio.
| Escenario | Frecuencia real | Impacto si no idempotente | Detección fácil |
|---|---|---|---|
| Reenvío tras timeout | ~1% de entregas | Doble cargo/subida de estado | Sí (logs y dashboard Stripe) |
| HTTP 5xx o crash | 0.1-0.5% | Pérdida/corrupción estado | Sí (monitoring externo) |
| Race condition DB | <0.1% | Inconsistencia concurrente | No, hasta auditoría manual |
| Lambdas asesinadas (timeout) | <0.05% | Procesamiento parcial | No, salvo trazas APM |
Decisión para dirección/operaciones
- Un 0.5% de eventos de pago son duplicados o reintentados al menos una vez. Con 10k cobros/mes, son hasta 50 incidentes/mes potenciales.
- Cada fallo cuesta entre 30 y 120€ en soporte, reversos y pérdida de reputación.
Arquitectura serverless: SQS y Lambdas, desacoplar y controlar
Diagrama textual del flujo
- Stripe → endpoint público (API Gateway/Lambda, FastAPI en AWS Lambda o Vercel Functions).
- El endpoint no procesa el evento, sólo valida la signature y lo inserta en SQS (Upstash o AWS SQS FIFO, deduplicación nativa 5 min).
- Lambda/ECS/FastAPI worker lee desde SQS, procesa idempotente sobre PostgreSQL (Aurora), responde a Stripe sólo cuando SQS ack asegurado.
Separar ingestión de proceso disminuye el TTFT (time-to-first-token) a Stripe a <250ms, incluso cuando la lógica interna necesita I/O de 2-6s.
| Componente | Ventaja | Trade-off |
|---|---|---|
| AWS SQS FIFO | Deduplicación 5 min, orden garantizado | ~10 ms overhead, throughput limitado a 300 msg/s |
| Lambda concurrency | Autoescala, zero-admin | Control de cold starts±90ms, gestión DLQ separada |
| API Gateway (public webhook) | Cierre TCP inmediato, rechazo upfront | Mayor latencia 1st byte vs NGINX custom |
Ejemplo: Ingestar webhook en SQS con Node.js (Vercel Lambda)
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; // v3.589.0
import { buffer } from "micro";
import { verifyStripeSignature } from "./stripe-verify";
export default async function handler(req, res) {
const payload = (await buffer(req)).toString();
const signature = req.headers["stripe-signature"];
if (!verifyStripeSignature(payload, signature)) {
res.status(400).send("invalid signature");
return;
}
const sqs = new SQSClient({ region: "eu-west-1" });
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.QUEUE_URL!,
MessageBody: payload,
MessageDeduplicationId: JSON.parse(payload).id, // Stripe event id
MessageGroupId: "stripe-events"
}));
res.status(200).send("ok");
}
- El endpoint responde OK incluso si el procesamiento real tarda minutos.
- Los duplicados en los siguientes 5 minutos se descartan en SQS.
Hard idempotencia en el backend: upserts, claves únicas y transacciones
Para ingeniería: SQL robusto contra duplicados
- El único "oráculo" es el event id de Stripe (event.id), único por evento real.
- El schema incluye una tabla stripe_webhook_events con constraint UNIQUE(id_evento).
- Todo flujo se ejecuta en una transacción SERIALIZABLE: si el evento ya está, ignora procesamiento.
-- Table con constraint idempotente
CREATE TABLE stripe_webhook_events (
id_evento TEXT PRIMARY KEY,
tipo TEXT NOT NULL,
payload JSONB NOT NULL,
procesado_at TIMESTAMPTZ DEFAULT now()
);
-- Upsert robusto (Python asyncpg >=0.29)
WITH insert_try AS (
INSERT INTO stripe_webhook_events (id_evento, tipo, payload) VALUES ($1, $2, $3)
ON CONFLICT (id_evento) DO NOTHING
RETURNING 1
)
SELECT COALESCE(MAX(1), 0) AS inserted FROM insert_try;
- Garantiza que el cuerpo del evento se almacena una y solo una vez: logging y replay nativo.
- Permite retry seguro en caso de error downstream. Al reiniciar el worker, sólo hace falta volver a consumir de SQS.
Error handling en FastAPI + asyncpg
import asyncpg
import asyncio
async def process_event(conn, event):
try:
inserted = await conn.fetchval(
"""
WITH ins AS (
INSERT INTO stripe_webhook_events (id_evento, tipo, payload)
VALUES ($1, $2, $3)
ON CONFLICT (id_evento) DO NOTHING
RETURNING 1)
SELECT COALESCE(MAX(1), 0) FROM ins
""",
event['id'], event['type'], event
)
if inserted:
# Lógica de negocio aquí
await run_payment_flow(conn, event)
else:
# Evento duplicado: skip idempotente
pass
await conn.execute("UPDATE stripe_webhook_events SET procesado_at = now() WHERE id_evento = $1", event['id'])
except asyncpg.PostgresError as exc:
await conn.execute("ROLLBACK")
raise
- Manejo explícito de duplicados: nunca se dispara la lógica dos veces.
- El rollback permite reintentos controlados sin riesgo de doble aplicación incluso si Lambda se mata (SIGKILL) entre pasos.
Reintentos exponenciales y garantía de entrega real
Para ingenieros: control de retry y DLQ
- Workers Lambda leen de SQS con batchSize 1-5, visibility timeout 60-180s.
- Cualquier error no capturado hace requeue: SQS reintenta tras timeout, con maxReceiveCount=5-10 antes de dead-letter-queue (DLQ).
- La métrica crítica es el ratio eventos DLQ/eventos aceptados; objetivo: <0.001, validado en producción.
| Opción retry | Cooldown/retry min | Max reentrega | Coste extra ($/mes, 10k eventos) |
|---|---|---|---|
| No retry, sólo Stripe | - | Hasta 30 vía Stripe | 0 (pero mayor riesgo idempotente) |
| SQS + Lambda | 1-30min | 5-10 | 0.18-0.80 |
| SQS FIFO + dedup | ~5min | 5 (fifo window) | 0.25-1.10 |
| SQS + DLQ (10 intentos) | ~30min | 10 | +0.02 (prácticamente irrelevante) |
Para dirección: garantías y SLA explícitos
- Entrega asegurada a base de datos: 99.9999% (sólo DLQ requiere inspección manual, <1 evento/mes en 50k eventos).
- Stripe jamás ve pérdidas: la arquitectura puede demostrar este SLA en logs auditables en caso de disputa.
- Fácil rollback y replay desde stripe_webhook_events ante bug: no se pierde facturación ni hay brechas compliance.
Evaluando la robustez: logs, tracing y comprobación sistemática
Para ingenieros: observabilidad práctica
- Correlacionar event.id de Stripe con trace_ids en logs Lambda (AWS X-Ray, Datadog, OpenTelemetry Python/TypeScript >=1.17).
- Alerta automática en backlog SQS >20 eventos pendientes o cualquier inserción fallida en stripe_webhook_events.
- Reprocesamiento seguro: script async Python para volver a intentar eventos en DLQ (sin duplicar ningún pago).
from asyncpg import create_pool
import json, asyncio
async def replay_failed_events(dumpfile):
pool = await create_pool(dsn="postgresql://.../db")
async with pool.acquire() as conn:
with open(dumpfile) as f:
for line in f:
event = json.loads(line)
try:
await process_event(conn, event)
except Exception as e:
print(f"Error en event {event['id']}: {e}")
asyncio.run(replay_failed_events("dlq_dump.jsonl"))
- Operativa 100% auditable y recuperable: ningún evento atascado se pierde/choca con pagos previos.
Para dirección: métricas accionables
- SLA de entrega auditado: >99.9999%, comprobable cada día con queries directas sobre la tabla de eventos.
- Menos de 1 hora al mes en soporte por incidentes de duplicados vs >12h/mes promedio sin arquitectura idempotente (basado en nuestros despliegues 2023-24).
Impacto en la operación: ahorrar tiempo, evitar errores y demostrar control
- Tiempo de respuesta a Stripe: <250 ms, validado en entorno Lambda y Vercel.
- Coste total arquitectura (SQS+Lambda+Aurora): <8 €/mes hasta 100k eventos. Sin operaciones manuales salvo DLQ (ratio <0.001%).
- Reducción de incidentes de cobro duplicado >99.99% tras primer mes en producción. Soporte facturación baja a menos de 0.5 FTE.
- Auditoría instantánea de cualquier pago vía SQL sobre stripe_webhook_events.
¿Dudas sobre reintentos, duplicated events o picos de fallo? Un diagnóstico en agente404.com señala el cuello de botella en menos de 120 min, con cifras y logs.
Usar colas, claves únicas y control granular en reintentos no es opcional. Es la diferencia entre un backend auditado y crecimiento seguro, o un infierno de soporte y pérdidas por errores tontos en pagos. En Agente404 lo implementamos y auditamos con datos de producción, no con promesas.
Te resulto util?
Compartelo con quien pueda necesitarlo



