Sair de 1 mil para 100 mil mensagens por dia no WhatsApp não é "comprar plano maior" — é arquitetura. Uma instância sozinha banhe antes de chegar lá; um sistema sem fila desconecta sob carga; sem circuit breaker, um pico de erro vira cascata.
Este guia mostra como escalar de forma segura: limites reais por instância, divisão entre múltiplas instâncias, fila com retry, circuit breaker, padrão de "warmup pool" e as 4 métricas que importam.
Limites reais por instância (em 2026)
| Estado da instância | Volume seguro/dia | Cuidados |
|---|---|---|
| Recém-pareada (dia 1–7) | ≤ 50 | Warmup obrigatório |
| Em warmup (dia 8–14) | ≤ 200 | Crescer gradual |
| Madura (15+ dias) | 1.000–2.000 | Cadência humana |
| Madura premium (60+ dias, baixa denúncia) | 3.000–5.000 | Limite efetivo da plataforma |
Ou seja: 100k mensagens/dia exige 20–50 instâncias maduras, não uma só. Não há atalho.
Cálculo: quantas instâncias preciso?
volume_diario = 100.000
volume_por_instancia_madura = 2.500 (média conservadora)
margem_seguranca = 1.5x // pra absorver picos
instancias_minimas = ceil(volume_diario / volume_por_instancia_madura * margem_seguranca)
= ceil(100.000 / 2.500 * 1.5)
= 60 instâncias
Cada instância na ZAP API custa R$ 49 (1ª e 2ª) e R$ 29 (3ª+). 60 instâncias = R$ 49+R$ 49 + 58×R$ 29 = R$ 1.780/mês. Soma 100k mensagens/dia ≈ 3M mensagens/mês — custo unitário de ~R$ 0,0006/mensagem. Compare com SMS (R$ 0,08–0,15) ou Push (R$ 0,001–0,005).
Arquitetura: outbox pattern + fila distribuída
┌─────────────────┐
│ Sua aplicação │
└────────┬────────┘
│ insere no banco
▼
┌─────────────────┐ ┌──────────────────┐
│ outbox_jobs │◀───│ Lock distribuído │
│ (Postgres) │ │ (Redis SETNX) │
└────────┬────────┘ └──────────────────┘
│ poll a cada 100ms
▼
┌─────────────────┐ ┌──────────────────┐
│ OutboxWorker x4 │───▶│ Circuit breaker │
└────────┬────────┘ │ per instance │
│ └──────────────────┘
▼
┌─────────────────┐
│ ZAP API send │
└─────────────────┘
Por que outbox e não enviar direto?
- Atomicidade transacional: registrar mensagem + acionar envio na mesma transação. Se a aplicação cair entre os dois, mensagem fica na fila — não perdida.
- Retry sem lógica espalhada: tudo vive no worker.
- Backpressure natural: se ZAP API estiver lenta, a fila enche, mas o cliente que insere não fica bloqueado.
Schema da tabela outbox
CREATE TABLE outbox_jobs (
id BIGSERIAL PRIMARY KEY,
instance_id VARCHAR(64) NOT NULL,
phone VARCHAR(32) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(16) DEFAULT 'pending',
attempts INT DEFAULT 0,
next_retry_at TIMESTAMP DEFAULT now(),
last_error TEXT,
created_at TIMESTAMP DEFAULT now(),
sent_at TIMESTAMP
);
CREATE INDEX idx_outbox_pending
ON outbox_jobs (status, next_retry_at)
WHERE status = 'pending';
Worker (Node.js + BullMQ)
const { Worker } = require('bullmq');
new Worker('outbox', async (job) => {
const { instanceId, phone, payload } = job.data;
// Circuit breaker per-instance
if (await breaker.isOpen(instanceId)) {
throw new Error('CircuitOpen');
}
try {
await zapApi.send({ instanceId, phone, ...payload });
await db.outbox.update({ status: 'sent', sentAt: new Date() },
{ where: { id: job.data.id } });
breaker.recordSuccess(instanceId);
} catch (err) {
breaker.recordFailure(instanceId, err);
if (err.statusCode === 429) {
// Rate limit — espera o Retry-After
throw new Error('RateLimit:' + err.headers['retry-after']);
}
throw err;
}
}, {
connection: redis,
concurrency: 4, // por worker
limiter: { max: 60, duration: 60_000 } // 60/min por instância
});
Circuit breaker per-instance
Sem circuit breaker, uma instância com problema (banimento, gateway down, etc) faz seus 4 workers ficarem girando em retry e queimar todo o Redis/DB. Implementação:
class CircuitBreaker {
constructor() {
this.state = new Map(); // instanceId → state
}
async isOpen(instanceId) {
const s = this.state.get(instanceId) || { failures: 0, openedAt: 0 };
if (s.openedAt && Date.now() - s.openedAt < 30_000) return true;
if (s.openedAt) this.state.set(instanceId, { failures: 0, openedAt: 0 }); // reabre
return false;
}
recordFailure(instanceId, err) {
const s = this.state.get(instanceId) || { failures: 0, openedAt: 0 };
s.failures++;
if (s.failures >= 3 && err.statusCode >= 500) {
s.openedAt = Date.now();
console.warn(`Circuit opened for ${instanceId}`);
}
this.state.set(instanceId, s);
}
recordSuccess(instanceId) {
this.state.set(instanceId, { failures: 0, openedAt: 0 });
}
}
Padrão "Warmup Pool"
Não pode ter todas as instâncias maduras desde o dia 1 — chip novo entra em warmup. Solução: dividir em 3 pools:
| Pool | Ocupação típica | Tipo de tráfego |
|---|---|---|
| Hot (maduras 60+ dias) | 70% das instâncias | Alta volume, marketing, broadcast |
| Warm (maduras 15–60 dias) | 20% | Volume médio, transacional |
| Cold/Warmup (0–14 dias) | 10% | Apenas inbound + low-volume outbound |
Roteador escolhe pool por critério:
function escolherInstancia(tipoMensagem) {
if (tipoMensagem === 'broadcast') return poolHot.next();
if (tipoMensagem === 'transactional') return poolWarm.next();
return poolHot.next(); // default
}
A cada 15 dias, promova as 10% mais antigas do warm para hot, e adicione novas instâncias ao cold.
As 4 métricas que importam
- Throughput entregue (msg/min): deveria estar perto da capacidade nominal das instâncias hot. Queda repentina = investigar.
- Taxa de erro 4xx/5xx (%): > 2% = alerta. > 10% = pause envios e investigue.
- Tempo médio na fila (p95): idealmente < 30s. Se > 5min, falta capacidade — adicione instâncias.
- Disconnect streak por instância: conta de desconexões consecutivas. > 3 em 1h = remover do pool, investigar.
A ZAP API expõe tudo isso em /super-admin/instances com sparkline 24h/7d/30d e endpoint agregado GET /v1/super-admin/summary.
Custo real total para 100k/dia
| 60 instâncias | R$ 1.780/mês |
| Chips pré-pagos (1x R$ 30 cada) | R$ 1.800 (one-time) |
| Smartphones para celulares vinculados (15× R$ 700, 4 instâncias por device) | R$ 10.500 (one-time) |
| Infra (Redis cluster + Postgres + workers) | R$ 800/mês |
| Total mensal recorrente | R$ 2.580 |
| Custo unitário por mensagem | ~R$ 0,0009 |
FAQ
- Tem como pular o warmup?
Não com risco aceitável. Pode comprar instâncias "envelhecidas" no mercado, mas é prática cinza e o ban também vem. - Posso vincular vários WhatsApp num único celular?
Sim — até 4 sessões multidevice por celular vinculado. Acima disso, conflitos. - Vale a pena criar uma instância "VIP" para clientes pagantes?
Sim — separe pool transacional (entrega rápida, baixa fila) do marketing (tolera fila maior). - O que faço se uma instância banir no meio da operação?
Remove do pool no roteador. Volume é redistribuído entre as outras automaticamente. Esse é o motivo de margem 1.5x no cálculo. - Posso usar Cloud API oficial pra escalar?
Pode, mas tem outros limites e custo por categoria de mensagem. Para escalar com previsibilidade técnica e financeira, multi-instância via API não-oficial costuma sair mais barato e com menos burocracia.
Criar conta para escalar (descontos a partir da 3ª instância) — trial 7 dias.