Envío de Emails en Lote (Batch)

Cuando necesitas enviar emails a múltiples destinatarios con contenido personalizado para cada uno, la función resend.batch.send() te permite enviar hasta 100 emails en una sola petición HTTP. Cada email puede tener diferente destinatario, asunto, contenido y configuración.

La función batch.send()

typescript
import { resend } from '@/lib/resend'
 
const { data, error } = await resend.batch.send([
  {
    from: 'Mi App <noreply@midominio.com>',
    to: ['usuario1@email.com'],
    subject: 'Hola Carlos',
    html: '<p>Tu reporte personalizado está listo.</p>',
  },
  {
    from: 'Mi App <noreply@midominio.com>',
    to: ['usuario2@email.com'],
    subject: 'Hola María',
    html: '<p>Tu reporte personalizado está listo.</p>',
  },
  {
    from: 'Mi App <noreply@midominio.com>',
    to: ['usuario3@email.com'],
    subject: 'Hola Pedro',
    html: '<p>Tu reporte personalizado está listo.</p>',
  },
])

La función recibe un array de objetos, donde cada objeto tiene la misma estructura que resend.emails.send(). Todos los parámetros que ya conoces (from, to, subject, html, react, text, attachments, tags, etc.) están disponibles para cada email individual.

El objeto de respuesta

typescript
// Respuesta exitosa
{
  data: {
    data: [
      { id: 'email-id-1' },
      { id: 'email-id-2' },
      { id: 'email-id-3' },
    ]
  },
  error: null
}

Resend te devuelve un array de objetos { id }, uno por cada email en el batch. El orden corresponde al orden en que pasaste los emails en el array.

Cuándo usar batch

El envío en lote es útil cuando:

  • Notificaciones a un equipo: un usuario crea una tarea y todos los miembros del equipo reciben una notificación personalizada
  • Emails de onboarding: varios usuarios se registraron y quieres enviarles el email de bienvenida con su nombre
  • Alertas del sistema: algo pasó en producción y necesitas notificar a todos los administradores
  • Reportes personalizados: cada usuario recibe un resumen con sus propios datos

Cada email puede ser diferente

A diferencia de enviar un email con múltiples destinatarios en to (donde todos reciben exactamente lo mismo), en batch cada email es independiente:

typescript
const miembros = [
  { email: 'carlos@empresa.com', nombre: 'Carlos', rol: 'admin' },
  { email: 'maria@empresa.com', nombre: 'María', rol: 'developer' },
  { email: 'pedro@empresa.com', nombre: 'Pedro', rol: 'designer' },
]
 
const emails = miembros.map((miembro) => ({
  from: 'Mi App <noreply@midominio.com>',
  to: [miembro.email],
  subject: `${miembro.nombre}, tienes una nueva tarea asignada`,
  html: `
    <p>Hola ${miembro.nombre},</p>
    <p>Se te ha asignado una nueva tarea en el proyecto.</p>
    <p>Tu rol actual: ${miembro.rol}</p>
    <a href="https://miapp.com/tareas">Ver mis tareas</a>
  `,
  tags: [
    { name: 'tipo', value: 'notificacion-tarea' },
    { name: 'rol', value: miembro.rol },
  ],
}))
 
const { data, error } = await resend.batch.send(emails)

Cada destinatario recibe un email personalizado y solo ve su propia dirección. No hay CC ni BCC implícitos.

Límites y rate limiting

RestricciónValor
Emails por petición batchMáximo 100
Rate limit2 requests por segundo por equipo (team)
Tamaño total de la peticiónLos mismos límites que emails individuales

Si necesitas enviar más de 100 emails, divide tu lista en chunks (bloques) de 100:

typescript
import { resend } from '@/lib/resend'
 
interface EmailData {
  email: string
  nombre: string
}
 
async function enviarNotificacionMasiva(usuarios: EmailData[]) {
  const BATCH_SIZE = 100
  const resultados: string[] = []
 
  for (let i = 0; i < usuarios.length; i += BATCH_SIZE) {
    const chunk = usuarios.slice(i, i + BATCH_SIZE)
 
    const emails = chunk.map((usuario) => ({
      from: 'Mi App <noreply@midominio.com>' as const,
      to: [usuario.email],
      subject: `${usuario.nombre}, actualización importante`,
      html: `<p>Hola ${usuario.nombre}, tenemos novedades.</p>`,
    }))
 
    const { data, error } = await resend.batch.send(emails)
 
    if (error) {
      console.error(`Error en batch ${i / BATCH_SIZE + 1}:`, error.message)
      continue
    }
 
    const ids = data.data.map((item) => item.id)
    resultados.push(...ids)
 
    // Respetar el rate limit de 2 requests/segundo
    if (i + BATCH_SIZE < usuarios.length) {
      await new Promise((resolve) => setTimeout(resolve, 500))
    }
  }
 
  return resultados
}
Respeta el rate limit

El rate limit de Resend para batch es de 2 requests por segundo por equipo. Si envías requests más rápido, recibirás un error rate_limit_exceeded. Implementa una pausa de al menos 500ms entre peticiones batch consecutivas.

Batch vs Broadcast: cuándo usar cada uno

Batch (resend.batch.send)Broadcast (Audiencias)
ControlProgramático, desde tu códigoDashboard o API de Audiencias
PersonalizaciónTotal, cada email es diferenteVariables en templates
Límite100 por peticiónMiles de contactos
Caso de usoNotificaciones, alertas, emails transaccionales a gruposNewsletters, campañas de marketing
Gestión de contactosTú la manejasResend la maneja (suscripciones, desuscripciones)
Para miles de contactos usa Broadcasts con Audiencias

Si necesitas enviar un newsletter o una campaña a miles de contactos, no uses batch. Usa la funcionalidad de Broadcasts con Audiencias de Resend. Broadcasts gestiona automáticamente las suscripciones, desuscripciones y el cumplimiento de regulaciones como CAN-SPAM. Batch está diseñado para envíos programáticos donde tú controlas la lista de destinatarios desde tu código.

Ejemplo real: notificar a todos los miembros de un equipo

Un caso de uso concreto: un usuario sube un archivo al proyecto y quieres notificar a todo el equipo.

typescript
import { resend } from '@/lib/resend'
 
interface TeamMember {
  email: string
  nombre: string
}
 
interface ArchivoSubido {
  nombre: string
  tamano: string
  url: string
  subidoPor: string
}
 
async function notificarEquipo(equipo: TeamMember[], archivo: ArchivoSubido) {
  const emails = equipo.map((miembro) => ({
    from: 'Proyectos <proyectos@midominio.com>' as const,
    to: [miembro.email],
    subject: `${archivo.subidoPor} subió un archivo al proyecto`,
    html: `
      <p>Hola ${miembro.nombre},</p>
      <p><strong>${archivo.subidoPor}</strong> acaba de subir un nuevo archivo:</p>
      <ul>
        <li>Archivo: ${archivo.nombre}</li>
        <li>Tamaño: ${archivo.tamano}</li>
      </ul>
      <p><a href="${archivo.url}">Descargar archivo</a></p>
    `,
    tags: [
      { name: 'tipo', value: 'notificacion-archivo' },
    ],
  }))
 
  const { data, error } = await resend.batch.send(emails)
 
  if (error) {
    console.error('Error notificando al equipo:', error.message)
    throw new Error(`Falló la notificación: ${error.message}`)
  }
 
  console.log(`${equipo.length} miembros notificados exitosamente`)
  return data.data.map((item) => item.id)
}

Manejo de errores en batch

Si alguno de los emails en el batch tiene un error de validación, todo el batch falla. No hay envío parcial. Asegúrate de validar los datos antes de enviar:

typescript
function validarEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
 
const emailsValidos = usuarios
  .filter((u) => validarEmail(u.email))
  .map((u) => ({
    from: 'Mi App <noreply@midominio.com>' as const,
    to: [u.email],
    subject: 'Notificación',
    html: `<p>Hola ${u.nombre}</p>`,
  }))
 
if (emailsValidos.length > 0) {
  const { data, error } = await resend.batch.send(emailsValidos)
}