---
title: "Envío masivo por WhatsApp: cola, rate limit y reintentos"
description: "Un bucle for con 5.000 contactos falla a la mitad y no sabes en cuáles. Cómo armar la cola, respetar el límite de la API, reintentar solo los errores que valen y retomar una campaña interrumpida sin enviar nada dos veces."
url: "https://api-wa.me/es/blog/cola-rate-limit-reintentos-envio-masivo-whatsapp"
language: "es"
og:type: "article"
og:site_name: "WAME API"
---

[Inicio](https://api-wa.me/es)/[Blog de la API de WhatsApp](https://api-wa.me/es/blog)/Envío masivo por WhatsApp: cola, rate limit y reintentos (lo que un bucle for no resuelve)

Raphael Serafim· Publicado el 10 de septiembre de 2026· 10 min de lectura

Compartir

# Envío masivo por WhatsApp: cola, rate limit y reintentos (lo que un bucle for no resuelve)

Un bucle for con 5.000 contactos falla a la mitad y no sabes en cuáles. Cómo armar la cola, respetar el límite de la API, reintentar solo los errores que valen y retomar una campaña interrumpida sin enviar nada dos veces.

Copiar para LLM[Ver como Markdown](https://api-wa.me/es/blog/cola-rate-limit-reintentos-envio-masivo-whatsapp.md)

**El código que todos escriben primero:**

```js
for (const contacto of contactos) {
  await enviar(contacto.numero, mensaje);
}
```

Funciona con 50 contactos. Con 5.000 falla a la mitad — y no sabes en cuál.

Este artículo es sobre la ingeniería del envío. La [lista](https://api-wa.me/es/blog/lista-contactos-optin-optout-whatsapp) es requisito previo: ninguna cola salva a una base mala.

## Los cuatro problemas del bucle

**1\. No tiene memoria.** ¿El proceso se cayó en el mensaje 2.300? Volver a ejecutarlo manda todo de nuevo.

**2\. No tiene ritmo.** Los primeros salen en milisegundos, alcanzas el límite y el resto falla en cadena.

**3\. No distingue errores.** "El número no tiene WhatsApp" y "servidor ocupado, intenta luego" caen en el mismo `catch`.

**4\. No es observable.** Mientras corre, nadie sabe cuántos salieron, cuántos fallaron ni cuánto falta.

## La estructura: productor, cola, worker

```js
import { Queue, Worker } from 'bullmq';

const cola = new Queue('campana', { connection: redis });

// PRODUCTOR — solo encola. No envía nada. Termina en segundos.
async function agendarCampana(campanaId, contactos, plantilla) {
  for (const c of contactos) {
    await cola.add('envio', {
      campanaId,
      to: c.numero,
      params: [c.nombre, c.pedido],
      plantilla,
    }, {
      // Determinista: reencolar la campaña entera no duplica nada.
      jobId: `${campanaId}:${c.numero}`,
      attempts: 5,
      backoff: { type: 'exponential', delay: 2000 },
    });
  }
}
```

El `jobId` es la pieza central. Con él, "correr la campaña de nuevo" es una operación segura: la cola descarta lo ya procesado y ejecuta solo lo que faltaba.

## El worker: uno por vez, al ritmo correcto

```js
new Worker('campana', async (job) => {
  const { to, params, plantilla, campanaId } = job.data;

  // Opt-out ACÁ, no al armar la campaña. Una campaña grande tarda
  // horas en drenar, y quien pide salir a la mitad tiene que dejar
  // de recibir a la mitad.
  if (await estaEnOptOut(to)) {
    await registrar(campanaId, to, 'optout');
    return;
  }

  try {
    const r = await enviarPlantilla(to, plantilla, params);
    await registrar(campanaId, to, 'enviado', r.messageId);
  } catch (e) {
    if (definitivo(e)) {
      await registrar(campanaId, to, 'fallo_definitivo', null, e.code);
      return;                       // no reintenta: no sirve
    }
    throw e;                        // temporal: deja que la cola reintente
  }
}, {
  connection: redis,
  concurrency: 1,                   // uno por vez: el ritmo es el objetivo
  limiter: { max: 20, duration: 60_000 },   // máximo 20 por minuto
});
```

Dos configuraciones hacen el trabajo:

**`concurrency: 1`** — paralelizar el envío es contraproducente. El cuello de botella es el límite de la plataforma, no tu CPU.

**`limiter`** — el techo duro. Aunque la cola tenga 50 mil elementos, salen 20 por minuto.

## Distinguir temporal de definitivo

Es lo que separa una cola que drena de una que se arrastra:

```js
const DEFINITIVOS = new Set([
  'numero_invalido',
  'sin_whatsapp',
  'plantilla_no_aprobada',
  'contacto_bloqueo',
]);

function definitivo(e) {
  if (DEFINITIVOS.has(e.code)) return true;

  const s = e.status;
  if (!s) return false;                 // red: temporal
  if (s === 429) return false;          // límite: temporal
  if (s >= 500) return false;           // servidor: temporal
  return s >= 400 && s < 500;           // demás 4xx: definitivo
}
```

Reintentar cinco veces un "el número no tiene WhatsApp" gasta cinco veces la cuota y atrasa a quien viene detrás, sin ninguna posibilidad de éxito.

## Backoff con jitter

El reintento sincronizado es peor que ninguno: si 200 mensajes fallan juntos por un `429` y todos reintentan a los 2 segundos, vuelven juntos y se llevan otro `429`.

```js
backoff: {
  type: 'custom',
  // 2s, 4s, 8s, 16s… con hasta 30% de variación aleatoria,
  // para que los reintentos no vuelvan todos en el mismo instante.
  strategy: (intento) => {
    const base = Math.min(2000 * 2 ** (intento - 1), 5 * 60_000);
    return base * (1 + Math.random() * 0.3);
  },
}
```

El techo de 5 minutos evita que el quinto intento caiga dentro de horas.

## Ritmo humano en la API no oficial

En la [API no oficial](https://api-wa.me/es/blog/api-no-oficial-whatsapp-que-es) no hay un límite documentado: hay el comportamiento del número, y una cadencia de robot es una de las señales que tumban cuentas.

```js
async function pausaHumana() {
  // 3 a 10 segundos, variable. Un intervalo fijo es un patrón detectable.
  const ms = 3000 + Math.random() * 7000;
  await new Promise((r) => setTimeout(r, ms));
}
```

Súmale pausas más largas cada cierto bloque y respeto al horario comercial — enviar a las 3 de la mañana genera bloqueos incluso con un opt-in impecable:

```js
function dentroDelHorario() {
  const ahora = new Date();
  const h = ahora.getHours();
  const dia = ahora.getDay();
  if (dia === 0) return false;              // domingo no
  if (dia === 6) return h >= 9 && h < 13;   // sábado por la mañana
  return h >= 8 && h < 20;
}
```

En el worker, si está fuera de horario, posponlo en lugar de enviar:

```js
if (!dentroDelHorario()) {
  await job.moveToDelayed(Date.now() + 30 * 60_000);
  return;
}
```

## Seguir la campaña mientras corre

```js
async function progreso(campanaId) {
  const [esperando, activos, fallidos] = await Promise.all([
    cola.getWaitingCount(),
    cola.getActiveCount(),
    cola.getFailedCount(),
  ]);
  const enviados = await contarPorEstado(campanaId, 'enviado');

  return {
    enviados,
    esperando,
    activos,
    fallidos,
    // Con un limiter de 20/min se puede estimar el final de verdad
    terminaEn: `${Math.ceil(esperando / 20)} min`,
  };
}
```

Una campaña sin barra de progreso es una campaña en la que nadie confía — y alguien termina corriéndola de nuevo "por las dudas", que es justo el escenario que el `jobId` previene.

## Sin Redis

El mismo diseño cabe en una tabla:

```sql
CREATE TABLE envios (
  campana_id   BIGINT,
  numero       VARCHAR(20),
  estado       VARCHAR(20) DEFAULT 'pendiente',
  intentos     INT DEFAULT 0,
  proximo_en   TIMESTAMPTZ DEFAULT NOW(),
  message_id   VARCHAR(80),
  error        VARCHAR(50),
  PRIMARY KEY (campana_id, numero)
);
```

La clave primaria compuesta da la idempotencia que daría el `jobId`. Un worker busca los pendientes con `proximo_en <= NOW()`, envía y actualiza. Es más código, y funciona.

Lo que **no** funciona es guardar el estado solo en la memoria del proceso: se pierde exactamente cuando más lo necesitas.

## Conclusión

El envío masivo es un problema de cola, no de bucle. Las cuatro piezas —idempotencia por `jobId`, limitador de ritmo, reintento solo de lo temporal y progreso visible— son media tarde de trabajo y convierten "lo corrí y no sé qué pasó" en una operación que retomas sin miedo.

### ¿Listo para automatizar tu WhatsApp?

Crea tu cuenta gratis y empieza a enviar mensajes por la API en minutos.

[Empezar gratis](https://portal.api-wa.me/sign-up)

## Preguntas frecuentes

¿Por qué no debo usar un bucle for para el envío masivo?+

Porque el bucle no tiene memoria. Si el proceso se cae en el mensaje 2.300 de 5.000, no sabes cuáles ya salieron, y volver a ejecutarlo se lo manda de nuevo a quien ya lo recibió. Además, un bucle sin control de ritmo alcanza el rate limit y empieza a recibir error en la mayoría de las llamadas restantes.

¿Cuál es el intervalo correcto entre mensajes?+

Depende del canal y del historial del número. En la API no oficial, algo entre 3 y 10 segundos con variación aleatoria es prudente. En la oficial el límite es más alto y está documentado, pero sigue existiendo. Lo correcto es tratar el intervalo como configuración ajustable, no como un número fijo en el código.

¿Qué errores conviene reintentar y cuáles no?+

Conviene reintentar el error temporal: 429 por límite, 5xx del servidor, timeout y fallo de red. No conviene reintentar el definitivo: número inválido, sin WhatsApp, plantilla no aprobada o contacto en opt-out. Reintentar un error definitivo gasta cuota y atrasa la cola sin ninguna posibilidad de éxito.

¿Cómo retomo una campaña que se cortó a la mitad?+

Con un jobId determinista por destinatario y campaña. Al reencolar todo, la cola descarta los trabajos ya procesados por el id repetido y solo corre lo que faltaba. Eso convierte el reenvío de la campaña completa en una operación segura.

¿Necesito Redis para esto?+

No necesariamente. Redis con BullMQ es el camino más corto, pero una tabla en la base con estado por destinatario y un worker leyendo los pendientes resuelve el mismo problema. Lo que no funciona es mantener el estado solo en la memoria del proceso, porque se pierde justo cuando más lo necesitas.

## Sigue leyendo

[### API de Instagram y Messenger en la misma API de WhatsApp: una instancia, un estándar

Tres canales oficiales de Meta casi siempre significan tres integraciones. Con una sola instancia, el mismo envío y el mismo sobre de webhook, solo cambia un campo: provider. Cómo funciona y qué te ahorra.](https://api-wa.me/es/blog/api-instagram-messenger-misma-api-whatsapp)[### API no oficial de WhatsApp: qué es, si es segura y cómo usarla (2026)

Entiende qué es una API no oficial de WhatsApp, cómo funciona la conexión por código QR, si es segura y legal, cuál es el riesgo real de bloqueo y cuándo conviene usarla en lugar de la API oficial.](https://api-wa.me/es/blog/api-no-oficial-whatsapp-que-es)[### API oficial vs no oficial de WhatsApp: ¿cuál elegir? (comparativa 2026)

Comparativa completa entre la API oficial de WhatsApp (Cloud API de Meta) y la API no oficial: costo, aprobación, límites de envío, plantillas, soporte y riesgo de bloqueo. Descubre cuál tiene sentido para tu caso.](https://api-wa.me/es/blog/api-whatsapp-oficial-vs-no-oficial)

[Volver al blog](https://api-wa.me/es/blog)

## Structured data

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "WAME API",
  "alternateName": "API Oficial y No Oficial de WhatsApp, Instagram y Messenger",
  "url": "https://api-wa.me",
  "inLanguage": "pt-BR",
  "publisher": {
    "@id": "https://api-wa.me/#organization",
    "@type": "Organization",
    "name": "WAME API",
    "url": "https://api-wa.me"
  }
}
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "@id": "https://api-wa.me/#organization",
  "name": "WAME API",
  "alternateName": [
    "WAME",
    "Wame API",
    "wame.api.br",
    "api-wa.me"
  ],
  "url": "https://api-wa.me",
  "logo": {
    "@type": "ImageObject",
    "url": "https://api-wa.me/images/web-app-manifest-512x512.png",
    "width": 512,
    "height": 512
  },
  "disambiguatingDescription": "WAME API é uma empresa brasileira de software, fundada em 2017 e parceira oficial da Meta (Meta Business Partner), que fornece APIs de WhatsApp, Instagram Direct e Messenger. Não tem relação com o wa.me, que é o encurtador de links operado pela WhatsApp LLC.",
  "identifier": {
    "@type": "PropertyValue",
    "propertyID": "INPI-BR",
    "name": "Pedido de registro de marca (INPI, classe NCL 42)",
    "value": "944724159"
  },
  "foundingDate": "2017",
  "slogan": "Socio Oficial de Meta — WhatsApp, Instagram y Messenger en una sola instancia. Desde 2017.",
  "description": "Plataforma brasileña y Socio Oficial de Meta (Meta Business Partner) para las APIs oficiales de WhatsApp (Cloud API), Instagram Direct y Messenger — las tres en una única instancia, con los mismos endpoints y un único formato de webhook. También ofrece la API no oficial vía Código QR, en la misma plataforma. En el mercado desde 2017, con más de 50 mil instancias creadas, 99,9% de uptime y soporte humano 24/7. SDKs oficiales para Node.js/TypeScript y PHP.",
  "knowsAbout": [
    "WhatsApp Cloud API oficial (Meta)",
    "API oficial de Instagram (Direct)",
    "API oficial de Messenger",
    "API multicanal Meta",
    "Meta Business Partner",
    "WhatsApp API",
    "API não oficial de WhatsApp",
    "automação de WhatsApp",
    "números virtuais",
    "webhooks"
  ],
  "sameAs": [
    "https://github.com/wame-api",
    "https://www.linkedin.com/company/wameapi",
    "https://www.instagram.com/wame.api/",
    "https://www.youtube.com/@wameapi"
  ],
  "contactPoint": {
    "@type": "ContactPoint",
    "contactType": "customer support",
    "url": "https://api-wa.me/contact",
    "availableLanguage": [
      "Portuguese"
    ]
  }
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Envío masivo por WhatsApp: cola, rate limit y reintentos (lo que un bucle for no resuelve)",
  "description": "Un bucle for con 5.000 contactos falla a la mitad y no sabes en cuáles. Cómo armar la cola, respetar el límite de la API, reintentar solo los errores que valen y retomar una campaña interrumpida sin enviar nada dos veces.",
  "image": "https://api-wa.me/es/blog/cola-rate-limit-reintentos-envio-masivo-whatsapp/opengraph-image",
  "datePublished": "2026-09-10",
  "dateModified": "2026-09-10",
  "author": {
    "@type": "Person",
    "name": "Raphael Serafim",
    "url": "https://github.com/raphaelvserafim",
    "sameAs": [
      "https://github.com/raphaelvserafim"
    ]
  },
  "publisher": {
    "@type": "Organization",
    "name": "api-wa.me",
    "logo": {
      "@type": "ImageObject",
      "url": "https://api-wa.me/images/screenshot.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://api-wa.me/es/blog/cola-rate-limit-reintentos-envio-masivo-whatsapp"
  },
  "keywords": "rate limit whatsapp api, cola de mensajes whatsapp, envío masivo whatsapp api, reintentos whatsapp api, enviar muchos mensajes whatsapp, backoff exponencial api, campaña whatsapp api",
  "inLanguage": "es"
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://api-wa.me"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog de la API de WhatsApp",
      "item": "https://api-wa.me/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Envío masivo por WhatsApp: cola, rate limit y reintentos (lo que un bucle for no resuelve)",
      "item": "https://api-wa.me/es/blog/cola-rate-limit-reintentos-envio-masivo-whatsapp"
    }
  ]
}
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "¿Por qué no debo usar un bucle for para el envío masivo?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Porque el bucle no tiene memoria. Si el proceso se cae en el mensaje 2.300 de 5.000, no sabes cuáles ya salieron, y volver a ejecutarlo se lo manda de nuevo a quien ya lo recibió. Además, un bucle sin control de ritmo alcanza el rate limit y empieza a recibir error en la mayoría de las llamadas restantes."
      }
    },
    {
      "@type": "Question",
      "name": "¿Cuál es el intervalo correcto entre mensajes?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Depende del canal y del historial del número. En la API no oficial, algo entre 3 y 10 segundos con variación aleatoria es prudente. En la oficial el límite es más alto y está documentado, pero sigue existiendo. Lo correcto es tratar el intervalo como configuración ajustable, no como un número fijo en el código."
      }
    },
    {
      "@type": "Question",
      "name": "¿Qué errores conviene reintentar y cuáles no?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Conviene reintentar el error temporal: 429 por límite, 5xx del servidor, timeout y fallo de red. No conviene reintentar el definitivo: número inválido, sin WhatsApp, plantilla no aprobada o contacto en opt-out. Reintentar un error definitivo gasta cuota y atrasa la cola sin ninguna posibilidad de éxito."
      }
    },
    {
      "@type": "Question",
      "name": "¿Cómo retomo una campaña que se cortó a la mitad?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Con un jobId determinista por destinatario y campaña. Al reencolar todo, la cola descarta los trabajos ya procesados por el id repetido y solo corre lo que faltaba. Eso convierte el reenvío de la campaña completa en una operación segura."
      }
    },
    {
      "@type": "Question",
      "name": "¿Necesito Redis para esto?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No necesariamente. Redis con BullMQ es el camino más corto, pero una tabla en la base con estado por destinatario y un worker leyendo los pendientes resuelve el mismo problema. Lo que no funciona es mantener el estado solo en la memoria del proceso, porque se pierde justo cuando más lo necesitas."
      }
    }
  ]
}
```
