---
title: "Disparo em massa no WhatsApp: fila, rate limit e retry"
description: "Um laço for com 5.000 contatos falha na metade e você não sabe em quais. Como montar a fila, respeitar o rate limit da API, aplicar retry com backoff só nos erros que valem e retomar uma campanha interrompida sem enviar nada duas vezes."
url: "https://api-wa.me/blog/fila-rate-limit-retry-disparo-whatsapp"
language: "pt-BR"
og:type: "article"
og:site_name: "WAME API"
---

[Início](https://api-wa.me/)/[Blog da API do WhatsApp](https://api-wa.me/blog)/Disparo em massa no WhatsApp: fila, rate limit e retry (a engenharia que o for-loop não resolve)

Raphael Serafim· Publicado em 10 de setembro de 2026· 10 min de leitura

Compartilhar

# Disparo em massa no WhatsApp: fila, rate limit e retry (a engenharia que o for-loop não resolve)

Um laço for com 5.000 contatos falha na metade e você não sabe em quais. Como montar a fila, respeitar o rate limit da API, aplicar retry com backoff só nos erros que valem e retomar uma campanha interrompida sem enviar nada duas vezes.

Copiar para LLM[Ver como Markdown](https://api-wa.me/blog/fila-rate-limit-retry-disparo-whatsapp.md)

**O código que todo mundo escreve primeiro:**

```js
for (const contato of contatos) {
  await enviar(contato.numero, mensagem);
}
```

Funciona com 50 contatos. Com 5.000, falha na metade — e você não sabe em qual.

Este artigo é sobre a engenharia do disparo. A [lista](https://api-wa.me/blog/lista-contatos-optin-optout-whatsapp) e as [boas práticas de conteúdo](https://api-wa.me/blog/enviar-mensagens-em-massa-whatsapp) são pré-requisito: nenhuma fila salva uma base ruim.

## Os quatro problemas do laço

**1\. Não tem memória.** Processo caiu na mensagem 2.300? Reexecutar manda tudo de novo. Quem já recebeu, recebe duas vezes.

**2\. Não tem ritmo.** As primeiras saem em milissegundos, você bate o limite, e o resto falha em sequência.

**3\. Não distingue erro.** "Número não tem WhatsApp" e "servidor ocupado, tente de novo" caem no mesmo `catch`.

**4\. Não é observável.** Enquanto roda, ninguém sabe quantas saíram, quantas falharam nem quanto falta.

## A estrutura: produtor, fila, worker

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

const fila = new Queue('campanha', { connection: redis });

// PRODUTOR — só enfileira. Não envia nada. Termina em segundos.
async function agendarCampanha(campanhaId, contatos, template) {
  for (const c of contatos) {
    await fila.add('envio', {
      campanhaId,
      to: c.numero,
      params: [c.nome, c.pedido],
      template,
    }, {
      // Determinístico: reenfileirar a campanha inteira não duplica nada.
      jobId: `${campanhaId}:${c.numero}`,
      attempts: 5,
      backoff: { type: 'exponential', delay: 2000 },
    });
  }
}
```

O `jobId` é a peça central. Com ele, "rodar a campanha de novo" vira uma operação segura: a fila descarta o que já processou e executa só o que faltava. Sem ele, cada reexecução é um novo disparo completo.

## O worker: um de cada vez, no ritmo

```js
new Worker('campanha', async (job) => {
  const { to, params, template, campanhaId } = job.data;

  // Opt-out AQUI, não na montagem da campanha. Uma campanha grande
  // leva horas para escoar, e quem pede para sair no meio precisa
  // parar de receber no meio.
  if (await estaEmOptOut(to)) {
    await registrar(campanhaId, to, 'optout');
    return;
  }

  try {
    const r = await enviarTemplate(to, template, params);
    await registrar(campanhaId, to, 'enviado', r.messageId);
  } catch (e) {
    if (definitivo(e)) {
      await registrar(campanhaId, to, 'falha_definitiva', null, e.code);
      return;                       // não repete: não adianta
    }
    throw e;                        // temporário: deixa a fila repetir
  }
}, {
  connection: redis,
  concurrency: 1,                   // um por vez: ritmo é o objetivo
  limiter: { max: 20, duration: 60_000 },   // no máximo 20 por minuto
});
```

Duas configurações fazem o trabalho:

**`concurrency: 1`** — paralelizar disparo é contraproducente. O gargalo é o limite da plataforma, não a sua CPU.

**`limiter`** — o teto duro. Mesmo que a fila tenha 50 mil itens, saem 20 por minuto.

## Distinguir erro temporário de definitivo

É o que separa uma fila que escoa de uma fila que se arrasta:

```js
const DEFINITIVOS = new Set([
  'numero_invalido',
  'sem_whatsapp',
  'template_nao_aprovado',
  'contato_bloqueou',
]);

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

  const s = e.status;
  if (!s) return false;                 // rede: temporário
  if (s === 429) return false;          // limite: temporário
  if (s >= 500) return false;           // servidor: temporário
  return s >= 400 && s < 500;           // demais 4xx: definitivo
}
```

Repetir cinco vezes um "número não tem WhatsApp" gasta 5× a cota e atrasa quem está atrás na fila, sem nenhuma chance de sucesso.

O que cada código significa e o tratamento certo para cada um estão em [erros da API e como tratar](https://api-wa.me/blog/erros-api-whatsapp-como-tratar).

## Backoff com jitter

Retry sincronizado é pior que retry nenhum: se 200 mensagens falham juntas por um `429` e todas repetem em 2 segundos, elas voltam juntas e tomam `429` de novo.

```js
backoff: {
  type: 'custom',
  // 2s, 4s, 8s, 16s… com até 30% de variação aleatória,
  // para as tentativas não voltarem todas no mesmo instante.
  strategy: (tentativa) => {
    const base = Math.min(2000 * 2 ** (tentativa - 1), 5 * 60_000);
    return base * (1 + Math.random() * 0.3);
  },
}
```

O teto de 5 minutos evita que a quinta tentativa caia daqui a horas.

## Ritmo humano na API não oficial

Na [API não oficial](https://api-wa.me/blog/api-nao-oficial-whatsapp) não há limite documentado — há o comportamento do número, e cadência de robô é um dos sinais que derrubam conta:

```js
async function pausaHumana() {
  // 3 a 10 segundos, variável. Intervalo fixo é padrão detectável.
  const ms = 3000 + Math.random() * 7000;
  await new Promise((r) => setTimeout(r, ms));
}
```

Some a isso pausas maiores a cada bloco e respeito ao horário comercial — mandar às 3 da manhã gera bloqueio mesmo com opt-in impecável:

```js
function dentroDoHorario() {
  const agora = new Date();
  const h = agora.getHours();
  const dia = agora.getDay();
  if (dia === 0) return false;              // domingo não
  if (dia === 6) return h >= 9 && h < 13;   // sábado de manhã
  return h >= 8 && h < 20;
}
```

No worker, se estiver fora do horário, adie em vez de enviar:

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

Na API oficial o limite é maior e documentado, mas o princípio continua: [o que protege o número](https://api-wa.me/blog/whatsapp-api-sem-bloqueio-nao-perder-numero) é o comportamento, não a tecnologia.

## Acompanhar enquanto roda

```js
async function progresso(campanhaId) {
  const [aguardando, ativos, falhos] = await Promise.all([
    fila.getWaitingCount(),
    fila.getActiveCount(),
    fila.getFailedCount(),
  ]);
  const enviados = await contarPorStatus(campanhaId, 'enviado');

  return {
    enviados,
    aguardando,
    ativos,
    falhos,
    // Com limiter de 20/min, dá para estimar o fim de verdade
    terminaEm: `${Math.ceil(aguardando / 20)} min`,
  };
}
```

Campanha sem barra de progresso é campanha que ninguém confia — e alguém acaba rodando de novo "por garantia", que é exatamente o cenário que o `jobId` previne.

## Sem Redis

O mesmo desenho cabe numa tabela:

```sql
CREATE TABLE envios (
  campanha_id  BIGINT,
  numero       VARCHAR(20),
  status       VARCHAR(20) DEFAULT 'pendente',
  tentativas   INT DEFAULT 0,
  proxima_em   TIMESTAMPTZ DEFAULT NOW(),
  message_id   VARCHAR(80),
  erro         VARCHAR(50),
  PRIMARY KEY (campanha_id, numero)
);
```

A chave primária composta dá a idempotência que o `jobId` daria. Um worker busca os pendentes com `proxima_em <= NOW()`, envia, atualiza. É mais código, e funciona.

O que **não** funciona é guardar o estado só na memória do processo: ele se perde exatamente no momento em que você mais precisa dele.

## Conclusão

Disparo em massa é problema de fila, não de laço. As quatro peças — idempotência por `jobId`, limitador de ritmo, retry só no que é temporário e progresso visível — são meia tarde de trabalho e transformam "rodei e não sei o que aconteceu" em uma operação que você retoma sem medo.

Depois que a campanha escoa, o trabalho continua: [medir entrega, leitura e resposta](https://api-wa.me/blog/metricas-campanha-whatsapp-api) é o que diz se valeu a pena.

### Pronto para automatizar seu WhatsApp?

Crie sua conta gratuita e comece a enviar mensagens pela API em minutos.

[Começar grátis](https://portal.api-wa.me/sign-up)

## Perguntas frequentes

Por que não devo usar um for-loop para disparar em massa?+

Porque o laço não tem memória. Se o processo cair na mensagem 2.300 de 5.000, você não sabe quais já foram, e reexecutar manda tudo de novo para quem já recebeu. Além disso, um laço sem controle de ritmo atinge o rate limit e passa a receber erro para a maioria das chamadas restantes.

Qual o intervalo certo entre mensagens?+

Depende do canal e do histórico do número. Na API não oficial, algo entre 3 e 10 segundos com variação aleatória é o intervalo prudente. Na oficial o limite é mais alto e documentado, mas continua existindo. O certo é tratar o intervalo como configuração ajustável, não como número fixo no código.

Quais erros valem retry e quais não?+

Vale repetir erro temporário: 429 de limite, 5xx do servidor, timeout e falha de rede. Não vale repetir erro definitivo: número inválido, sem WhatsApp, template não aprovado ou contato em opt-out. Repetir erro definitivo gasta cota e atrasa a fila sem chance de sucesso.

Como retomar uma campanha que parou no meio?+

Com um jobId determinístico por destinatário e campanha. Ao reenfileirar tudo, a fila descarta os jobs já processados pelo id repetido, e só o que faltava roda. Isso torna o reenvio da campanha inteira uma operação segura.

Preciso de Redis para fazer isso?+

Não obrigatoriamente. Redis com BullMQ é o caminho mais curto, mas uma tabela no banco com status por destinatário e um worker lendo os pendentes resolve o mesmo problema. O que não funciona é manter o estado só na memória do processo, porque ele se perde exatamente quando você mais precisa dele.

## Continue lendo

[### Como criar um chatbot de IA com a API da OpenAI para responder no WhatsApp

Um webhook, uma chamada à API da OpenAI e uma resposta pela WAME API: o código completo de um chatbot de IA que atende no WhatsApp, Instagram e Messenger. Com memória por contato, controle de custo e o que fazer quando a IA não deve responder.](https://api-wa.me/blog/chatbot-ia-openai-whatsapp)[### Cobrança por Pix dentro do WhatsApp pela API: como enviar e o que muda na conversão

Mandar o código Pix no WhatsApp resolve o pior ponto da cobrança digital: o cliente não precisa sair do app. Como enviar a cobrança pela API, tratar a confirmação e evitar os erros que transformam a facilidade em suporte.](https://api-wa.me/blog/cobrar-pix-whatsapp-api)[### Erros da API do WhatsApp: o que cada um significa e como tratar

A mensagem não saiu e o log diz apenas 'erro ao enviar'. Os erros que você vai encontrar de verdade — janela fechada, número inválido, template não aprovado, limite atingido, instância caída — e o tratamento certo para cada um.](https://api-wa.me/blog/erros-api-whatsapp-como-tratar)

[Voltar ao blog](https://api-wa.me/blog)

## Structured data

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "WAME API",
  "alternateName": "API Oficial e Não Oficial de WhatsApp, Instagram e 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": "Parceira Oficial da Meta — WhatsApp, Instagram e Messenger numa instância só. Desde 2017.",
  "description": "Plataforma brasileira e Parceira Oficial da Meta (Meta Business Partner) para as APIs oficiais de WhatsApp (Cloud API), Instagram Direct e Messenger — os três numa única instância, com os mesmos endpoints e um único formato de webhook. Também oferece a API não oficial via QR Code, na mesma plataforma. No mercado desde 2017, com mais de 50 mil instâncias criadas, 99,9% de uptime e suporte humano 24/7 em português. SDKs oficiais para Node.js/TypeScript e 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": "Disparo em massa no WhatsApp: fila, rate limit e retry (a engenharia que o for-loop não resolve)",
  "description": "Um laço for com 5.000 contatos falha na metade e você não sabe em quais. Como montar a fila, respeitar o rate limit da API, aplicar retry com backoff só nos erros que valem e retomar uma campanha interrompida sem enviar nada duas vezes.",
  "image": "https://api-wa.me/blog/fila-rate-limit-retry-disparo-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/blog/fila-rate-limit-retry-disparo-whatsapp"
  },
  "keywords": "rate limit whatsapp api, fila de mensagens whatsapp, disparo em massa whatsapp api, retry whatsapp api, enviar muitas mensagens whatsapp, backoff exponencial api, campanha whatsapp api",
  "inLanguage": "pt-BR"
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://api-wa.me"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog da API do WhatsApp",
      "item": "https://api-wa.me/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Disparo em massa no WhatsApp: fila, rate limit e retry (a engenharia que o for-loop não resolve)",
      "item": "https://api-wa.me/blog/fila-rate-limit-retry-disparo-whatsapp"
    }
  ]
}
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Por que não devo usar um for-loop para disparar em massa?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Porque o laço não tem memória. Se o processo cair na mensagem 2.300 de 5.000, você não sabe quais já foram, e reexecutar manda tudo de novo para quem já recebeu. Além disso, um laço sem controle de ritmo atinge o rate limit e passa a receber erro para a maioria das chamadas restantes."
      }
    },
    {
      "@type": "Question",
      "name": "Qual o intervalo certo entre mensagens?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Depende do canal e do histórico do número. Na API não oficial, algo entre 3 e 10 segundos com variação aleatória é o intervalo prudente. Na oficial o limite é mais alto e documentado, mas continua existindo. O certo é tratar o intervalo como configuração ajustável, não como número fixo no código."
      }
    },
    {
      "@type": "Question",
      "name": "Quais erros valem retry e quais não?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Vale repetir erro temporário: 429 de limite, 5xx do servidor, timeout e falha de rede. Não vale repetir erro definitivo: número inválido, sem WhatsApp, template não aprovado ou contato em opt-out. Repetir erro definitivo gasta cota e atrasa a fila sem chance de sucesso."
      }
    },
    {
      "@type": "Question",
      "name": "Como retomar uma campanha que parou no meio?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Com um jobId determinístico por destinatário e campanha. Ao reenfileirar tudo, a fila descarta os jobs já processados pelo id repetido, e só o que faltava roda. Isso torna o reenvio da campanha inteira uma operação segura."
      }
    },
    {
      "@type": "Question",
      "name": "Preciso de Redis para fazer isso?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Não obrigatoriamente. Redis com BullMQ é o caminho mais curto, mas uma tabela no banco com status por destinatário e um worker lendo os pendentes resolve o mesmo problema. O que não funciona é manter o estado só na memória do processo, porque ele se perde exatamente quando você mais precisa dele."
      }
    }
  ]
}
```
