Resend en Producción: Monitorización, Analíticas y Gestión de Bounces que Nadie Te Explica
Aprende a implementar observabilidad completa en Resend: event tracking, gestión de bounces, analíticas y alertas proactivas para producción real.
Tu Pipeline de Email Es una Caja Negra
Envías el email. La API devuelve `200 OK`. Asumes que llegó.
Eso no es un sistema de email en producción. Es optimismo con sintaxis de JavaScript.
*El problema real no es enviar emails. Es saber exactamente qué pasa con cada uno después de enviarlo.*
La mayoría de developers configura Resend, verifica el dominio, implementa los templates con React Email, y da el trabajo por terminado. Hasta que un cliente dice que no recibió su email de confirmación. O hasta que tu tasa de entrega cae en picado y no sabes por qué.
Este artículo cubre la capa que falta: observabilidad completa del ciclo de vida de cada email usando los webhooks de eventos de Resend, gestión de bounces, y un sistema de alertas que te avisa antes de que el problema escale.
---
Por Qué el 80% de los Sistemas de Email Fallan en Silencio
Resend registra cada evento de cada email que envías: `email.sent`, `email.delivered`, `email.bounced`, `email.complained`, `email.opened`, `email.clicked`.
La mayoría de developers no los escucha.
El resultado: bounces acumulados que degradan tu reputación de dominio, usuarios marcando tus emails como spam sin que lo sepas, y direcciones inválidas que consumen cuota de envío sin generar valor.
La reputación de un dominio de email se construye durante meses y se destruye en días.
Una tasa de complaints por encima del 0,08% activa las protecciones de Gmail. Una tasa de bounces por encima del 2% puede llevar a la suspensión temporal de tu dominio en cualquier ESP.
Necesitas saber estos números antes de que lleguen a esos umbrales.
---
Arquitectura: Event Tracking Completo con Resend Webhooks
El punto de entrada es la configuración de webhooks en el dashboard de Resend. Activa todos los eventos: delivered, bounced, complained, opened, clicked.
Después, necesitas una tabla en tu base de datos para persistir cada evento.
```sql
CREATE TABLE email_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resend_email_id TEXT NOT NULL,
event_type TEXT NOT NULL,
recipient TEXT NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_email_events_recipient ON email_events(recipient);
CREATE INDEX idx_email_events_type ON email_events(event_type);
CREATE INDEX idx_email_events_resend_id ON email_events(resend_email_id);
```
Y tu endpoint de webhook que procesa y persiste:
```typescript
import { Resend } from 'resend';
import { createClient } from '@supabase/supabase-js';
import { NextRequest, NextResponse } from 'next/server';
const resend = new Resend(process.env.RESEND_API_KEY);
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('svix-signature') ?? '';
const msgId = req.headers.get('svix-id') ?? '';
const timestamp = req.headers.get('svix-timestamp') ?? '';
// Verifica la firma del webhook
const webhookSecret = process.env.RESEND_WEBHOOK_SECRET!;
try {
const { Webhook } = await import('svix');
const wh = new Webhook(webhookSecret);
const payload = wh.verify(body, {
'svix-id': msgId,
'svix-timestamp': timestamp,
'svix-signature': signature,
}) as { type: string; data: Record<string, unknown> };
const { type, data } = payload;
// Persiste el evento
await supabase.from('email_events').insert({
resend_email_id: data.email_id as string,
event_type: type,
recipient: data.to as string,
metadata: data,
});
// Gestión de bounces hard
if (type === 'email.bounced') {
await handleHardBounce(data.to as string, data);
}
// Alerta de complaint inmediata
if (type === 'email.complained') {
await handleComplaint(data.to as string);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook verification failed:', error);
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
}
```
---
Gestión de Bounces: El Patrón que Protege tu Reputación
No todos los bounces son iguales. La distinción crítica:
→ Hard bounce: dirección no existe o dominio inválido. Nunca vuelvas a enviar.
→ Soft bounce: buzón lleno, servidor temporalmente no disponible. Puedes reintentar.
Resend distingue ambos en el campo `bounce.type` del evento.
```typescript
async function handleHardBounce(
email: string,
data: Record<string, unknown>
) {
const bounceData = data.bounce as { type?: string };
if (bounceData?.type === 'hard') {
// Marca la dirección como inválida en tu base de datos
await supabase
.from('users')
.update({
email_status: 'hard_bounced',
email_suppressed_at: new Date().toISOString()
})
.eq('email', email);
// Añade a lista de supresión local
await supabase.from('email_suppressions').upsert({
email,
reason: 'hard_bounce',
suppressed_at: new Date().toISOString()
});
}
}
async function handleComplaint(email: string) {
// Suprime inmediatamente y notifica al equipo
await supabase.from('email_suppressions').upsert({
email,
reason: 'spam_complaint',
suppressed_at: new Date().toISOString()
});
// Notificación interna — usa tu sistema de alertas
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `⚠️ Spam complaint recibido de ${email}. Dirección suprimida automáticamente.`
})
});
}
```
El patrón de supresión local es obligatorio. Resend mantiene su propia lista de supresión, pero tú necesitas la tuya para prevenir intentos de envío antes de que el ciclo de webhook se complete.
---
Dashboard de Analíticas: Las Métricas que Importan
Con los eventos persistidos en Supabase, puedes construir queries que te den visibilidad real.
```typescript
// Resumen de métricas de los últimos 7 días
async function getEmailHealthMetrics() {
const { data } = await supabase.rpc('email_health_summary', {
days_back: 7
});
return data;
}
```
Y la función SQL correspondiente:
```sql
CREATE OR REPLACE FUNCTION email_health_summary(days_back INT)
RETURNS JSON AS $$
DECLARE
result JSON;
BEGIN
SELECT json_build_object(
'total_sent', COUNT(*) FILTER (WHERE event_type = 'email.sent'),
'total_delivered', COUNT(*) FILTER (WHERE event_type = 'email.delivered'),
'total_bounced', COUNT(*) FILTER (WHERE event_type = 'email.bounced'),
'total_complained', COUNT(*) FILTER (WHERE event_type = 'email.complained'),
'delivery_rate',
ROUND(
COUNT(*) FILTER (WHERE event_type = 'email.delivered')::NUMERIC /
NULLIF(COUNT() FILTER (WHERE event_type = 'email.sent'), 0) 100,
2
),
'bounce_rate',
ROUND(
COUNT(*) FILTER (WHERE event_type = 'email.bounced')::NUMERIC /
NULLIF(COUNT() FILTER (WHERE event_type = 'email.sent'), 0) 100,
2
)
) INTO result
FROM email_events
WHERE created_at >= NOW() - (days_back || ' days')::INTERVAL;
RETURN result;
END;
$$ LANGUAGE plpgsql;
```
Estos números son los que determinan la salud de tu pipeline de email.
---
Comparativa: Sin Observabilidad vs Con Observabilidad
❌ Sin sistema de eventos:
→ Envías a direcciones que llevan meses devolviendo hard bounces
→ No sabes que tu tasa de complaints superó el umbral crítico
→ Un cliente reporta que no recibe emails y tardas horas en diagnosticar
→ Tu dominio cae en carpetas de spam sin previo aviso
✅ Con event tracking completo:
→ Cada hard bounce suprime la dirección automáticamente
→ Alerta en Slack en tiempo real cuando hay un complaint
→ Dashboard con delivery rate, bounce rate y complaint rate por periodo
→ Queries que identifican patrones: ¿qué tipo de email tiene más bounces?
---
Alertas Proactivas: El Sistema que Te Avisa Antes del Problema
Un cron job que ejecuta cada hora y comprueba umbrales críticos:
```typescript
// Ejecuta con Vercel Cron o cualquier scheduler
export async function checkEmailHealthAlerts() {
const metrics = await getEmailHealthMetrics();
const alerts = [];
if (metrics.bounce_rate > 2) {
alerts.push(`🚨 Bounce rate crítico: ${metrics.bounce_rate}%`);
}
if (metrics.complaint_rate > 0.08) {
alerts.push(`🚨 Complaint rate peligroso: ${metrics.complaint_rate}%`);
}
if (metrics.delivery_rate < 95) {
alerts.push(`⚠️ Delivery rate bajo: ${metrics.delivery_rate}%`);
}
if (alerts.length > 0) {
await notifyTeam(alerts.join('\n'));
}
}
```
Esto no es opcional en producción. Es la diferencia entre enterarte de un problema por tus propios sistemas o por un cliente enfadado.
---
Lo Que Se Lleva de Aquí
La capa de observabilidad de un sistema de email en producción tiene tres componentes:
→ Event tracking: persiste cada webhook de Resend en tu base de datos
→ Suppression management: gestión local de bounces y complaints con acción inmediata
→ Proactive alerting: umbrales configurados que disparan notificaciones antes de que el daño sea irreversible
El real resend email api tutorial no termina cuando envías tu primer email. Termina cuando tienes visibilidad completa de qué pasa con cada mensaje, quién lo recibe, quién lo rechaza, y quién te está marcando como spam.
La arquitectura que no monitoriza su pipeline de email no tiene un sistema de email. Tiene una esperanza con una API key.
Implementa el event tracking hoy. Tu tasa de entrega a seis meses te lo agradecerá.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

