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.

Brian Mena

Brian Mena

Ingeniero informatico construyendo productos digitales rentables: SaaS, directorios y agentes de IA. Todo desde cero, todo en produccion.

LinkedIn