Enviar Emails en Lotes sin Romper tu API: La Guía Práctica con Resend
Hace tres meses integré Resend en un proyecto que envía notificaciones a cientos de usuarios. El primer día, intenté enviar emails a todos en un solo request. Falló. No porque Resend fuese malo, sino porque no entendía sus limitaciones.
Ahora lo sé: Resend tiene un máximo de 50 destinatarios por request. Punto. Si intentas pasar 500, simplemente rechaza el request. Y si no lo haces bien, pierdes entregas, dinero y confianza de tus usuarios.
Este artículo es lo que aprendí construyendo en público.
El Problema Real: No es Solo un Límite de 50
El límite de 50 destinatarios no es el problema. El problema es lo que pasa cuando tu aplicación crece y necesitas enviar emails a 5.000 usuarios a la vez.
¿Qué haces?
- ¿Haces 100 requests seguidos? Arriesgas que algunos fallen a mitad de camino.
- ¿Los haces en paralelo? Puedes saturar tu API y perder control.
- ¿Los haces secuencialmente? Tarda horas.
La respuesta está en tres conceptos: batching inteligente, idempotency keys y scheduling con lenguaje natural.
El Batching Inteligente: Dividir sin Perder
Primero, necesitas dividir tu lista en lotes de 50.
```javascript function batchRecipients(recipients, batchSize = 50) { const batches = []; for (let i = 0; i < recipients.length; i += batchSize) { batches.push(recipients.slice(i, i + batchSize)); } return batches; }
const allUsers = await db.users.findMany(); const batches = batchRecipients(allUsers);
console.log(`Enviando ${batches.length} lotes de emails`); ```
Esto es lo básico. Pero aquí viene lo importante: necesitas saber cuál de estos lotes falló y cuál no.
Las Idempotency Keys: Tu Red de Seguridad
Una idempotency key es un identificador único que le dices a Resend: "Si envío esto dos veces con la misma key, solo hazlo una vez".
¿Por qué importa? Porque tu código puede fallar a mitad de un batch. O la red puede cortarse. O tu servidor puede reiniciarse. Sin idempotency keys, envías el mismo email dos veces.
Con idempotency keys, Resend te dice: "Ya envié esto, aquí está el resultado".
```javascript import { Resend } from 'resend'; const resend = new Resend(process.env.RESEND_API_KEY);
async function sendBatch(batch, campaignId) { const batchId = `campaign_${campaignId}_batch_${Date.now()}`;
const result = await resend.emails.send({ from: 'notifications@tudominio.com', to: batch.map(user => user.email), subject: 'Tu notificación importante', html: '<p>Contenido aquí</p>', headers: { 'X-Entity-Ref-ID': batchId, // Resend usa esto como idempotency key }, });
return result; } ```
Ahora, si tu proceso se interrumpe y reintentas, Resend te devuelve el mismo resultado sin duplicar.
Scheduling en Lenguaje Natural: El Truco Subestimado
Aquí es donde muchos desarrolladores se equivocan. Intentan controlar el timing desde su código.
Resend tiene una feature mejor: puedes programar emails con fechas en lenguaje natural.
```javascript async function scheduleEmailCampaign(recipients, scheduledFor) { const batches = batchRecipients(recipients); const results = [];
for (let i = 0; i < batches.length; i++) { // Distribuir los lotes en el tiempo const delayMinutes = i * 2; // Cada lote, 2 minutos después const scheduledTime = new Date( new Date(scheduledFor).getTime() + delayMinutes * 60000 );
const result = await resend.emails.send({ from: 'campaigns@tudominio.com', to: batches[i].map(user => user.email), subject: 'Tu oferta especial', html: renderTemplate(batches[i]), scheduledAt: scheduledTime.toISOString(), headers: { 'X-Entity-Ref-ID': `campaign_batch_${i}_${Date.now()}`, }, });
results.push(result); }
return results; } ```
¿Por qué esto es mejor?
1. No ocupas recursos ahora: Los emails se envían después, cuando tengas capacidad. 2. Control distribuido: No envías 100 requests en paralelo que saturen todo. 3. Mejor experiencia del usuario: Si programas para las 9 AM, todos reciben el email a la misma hora.
El Stack Completo: Base de Datos + Resend + Tracking
En producción, necesitas rastrear qué pasó con cada batch.
```javascript async function trackCampaignBatch(campaignId, batchNumber, result) { await db.emailBatches.create({ campaignId, batchNumber, totalRecipients: result.to.length, resendId: result.id, status: result.id ? 'queued' : 'failed', sentAt: new Date(), }); }
async function sendCampaignSafely(campaignId, recipients) { const batches = batchRecipients(recipients);
for (let i = 0; i < batches.length; i++) { try { const result = await sendBatch(batches[i], campaignId); await trackCampaignBatch(campaignId, i, result); } catch (error) { console.error(`Batch ${i} falló:`, error); // Reintentar más tarde, o alertar await db.failedBatches.create({ campaignId, batchNumber: i, error: error.message, }); } } } ```
Ahora tienes:
- Qué lotes se enviaron
- Cuáles fallaron
- Cuándo reintentar
- Prueba de entrega para auditoría
El Error Común: Ignorar los Webhooks
Resend envía webhooks cuando un email se abre, rebota, o se marca como spam. Muchos desarrolladores los ignoran.
Error grave.
Esos webhooks te dicen:
- Si un email fue entregado realmente
- Si alguien se queja
- Si una dirección es inválida
```javascript // En tu endpoint de webhooks export async function POST(req) { const event = await req.json();
if (event.type === 'email.bounced') { // Marca este usuario como inentregable await db.users.update( { email: event.email }, { bounced: true } ); }
if (event.type === 'email.complained') { // Quítalo de futuras campañas await db.users.update( { email: event.email }, { unsubscribed: true } ); }
return Response.json({ ok: true }); } ```
Ignorar esto significa que seguirás enviando emails a direcciones muertas. Y eso daña tu reputación como remitente.
Números Reales de Mi Experiencia
Cuando implementé esto correctamente:
- **Antes**: Perdía entregas porque intentaba enviar todo en paralelo. Algunos lotes fallaban silenciosamente.
- **Después**: Cada email se rastrea. Sé exactamente qué se envió, cuándo, y si fue entregado.
La diferencia no es pequeña. Es la diferencia entre "creo que mi campaña funcionó" y "sé exactamente qué funcionó".
El Takeaway
Resend es simple hasta que crece. El límite de 50 no es una limitación, es un feature que te obliga a pensar en arquitectura.
Tres cosas para llevar:
1. Batch siempre en lotes de 50: No negocias con esto. 2. Usa idempotency keys: Tu código va a fallar, prepárate. 3. Programa con scheduling: Distribuye la carga, no la concentres.
Si haces esto bien, puedes enviar a miles de usuarios sin romper nada. Sin perder entregas. Sin duplicar emails.
Y eso es lo que separa a los que construyen en serio de los que construyen por jugar.
¿Estás enviando emails en producción? Revisa tu implementación. Probablemente haya algo que puedas mejorar.