Enviar Emails en Lotes sin Romper tu API: La Guía Práctica con Resend

Programación· 5 min de lectura

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.