Banimento de número WhatsApp é o pior pesadelo de quem opera em escala. Você acorda, abre o painel e vê instância em "BANNED" — número queimado, contatos perdidos, histórico inacessível. Recuperação é difícil e frequentemente impossível. A boa notícia: 90% dos banimentos seguem padrões previsíveis e dá para evitar com rate limiting inteligente.
Este artigo mostra a estratégia completa de throttling: 2 camadas de limite (API REST + comportamental WhatsApp), token bucket distribuído em Redis, ajuste dinâmico baseado em taxa de erro, e o que fazer quando você manda número novo entrar em produção.
As 2 camadas de rate limit
Camada 1: API REST
É o limite da própria ZAP API por instância. Existe para proteger nossa infraestrutura e a sua. Hoje em dia: 60 requisições/minuto por instância, retorna 429 com headers padrão.
// Headers em toda resposta:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1715812230 // unix timestamp do reset
Estoura esse limite e você recebe 429 Too Many Requests com Retry-After: N em segundos. Backoff e tente de novo.
Camada 2: Comportamental WhatsApp
Esse é o limite que importa para banimento. Não é documentado pela WhatsApp, mas baseado em milhões de mensagens enviadas, sabemos:
- Número novo (recém-conectado): não passe de 30 mensagens/hora na primeira semana, 100/hora na segunda, 300/hora a partir da terceira (warmup).
- Número aquecido (3+ meses ativos): 1.000-2.000 msgs/hora seguro
- Número antigo confiável (1+ ano): 5.000+ msgs/hora factível, mas evite explosões — distribua ao longo do dia
- Janela de 24h: nunca passe de 30k msgs/dia em um único número
- Mesma mensagem para muitos: reduzir score de "spam" — varie texto com spintax
- Velocidade de envio: não menos que 800ms entre mensagens (evita parecer robô)
Token bucket por instância em Redis
O algoritmo clássico para rate limiting com burst controlado. Cada instância tem um "balde de tokens" — toma 1 token por mensagem, balde reabastece a uma taxa fixa. Se balde vazio, mensagem espera.
// rate-limiter.ts
import { createClient } from "redis";
import Redlock from "redlock";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const redlock = new Redlock([redis as any]);
interface BucketConfig {
capacity: number; // tokens máximos no balde (burst)
refillPerSec: number; // tokens repostos por segundo
}
export async function takeToken(instanceId: string, cfg: BucketConfig) {
const key = `bucket:${instanceId}`;
const lock = await redlock.acquire([`lock:${key}`], 2000);
try {
const now = Date.now() / 1000;
const raw = await redis.get(key);
const state = raw ? JSON.parse(raw) : { tokens: cfg.capacity, ts: now };
// Reabastece com base no tempo decorrido
const elapsed = now - state.ts;
state.tokens = Math.min(cfg.capacity, state.tokens + elapsed * cfg.refillPerSec);
state.ts = now;
if (state.tokens < 1) {
// Quanto falta para 1 token?
const waitMs = Math.ceil((1 - state.tokens) / cfg.refillPerSec * 1000);
await redis.set(key, JSON.stringify(state), { EX: 3600 });
return { allowed: false, retryInMs: waitMs };
}
state.tokens -= 1;
await redis.set(key, JSON.stringify(state), { EX: 3600 });
return { allowed: true };
} finally {
await lock.release();
}
}
// Uso (ex.: número aquecido, 1k msgs/hora = 0.27 tokens/seg):
const result = await takeToken("inst_xyz", { capacity: 60, refillPerSec: 0.27 });
if (!result.allowed) {
console.log(`aguardar ${result.retryInMs}ms`);
return;
}
// envia mensagem
Throttle dinâmico baseado em erro
Token bucket fixo é bom, mas pode estar errado. Imagine: você configurou 1k msgs/hora, mas o WhatsApp está nervoso hoje e devolvendo 30% de erro. Continuar enviando piora a situação. Reduza a vazão quando erro sobe:
// dynamic-rate.ts
const ERROR_WINDOW_MS = 60_000;
const errors: Map = new Map();
function recordError(instanceId: string) {
const arr = errors.get(instanceId) ?? [];
arr.push(Date.now());
errors.set(instanceId, arr);
}
function recentErrorRate(instanceId: string): number {
const arr = errors.get(instanceId) ?? [];
const now = Date.now();
const recent = arr.filter(t => now - t < ERROR_WINDOW_MS);
errors.set(instanceId, recent);
// Assume 1 mensagem/seg de baseline; erros recentes / janela
return recent.length / (ERROR_WINDOW_MS / 1000);
}
function adjustedRate(baseRate: number, errorRate: number): number {
if (errorRate > 0.10) return baseRate * 0.3; // 30% original
if (errorRate > 0.05) return baseRate * 0.6;
return baseRate;
}
Sliding window vs token bucket vs leaky bucket
Comparação rápida:
- Token bucket: permite burst até a capacity, refill constante. Bom para "1.000 msgs/hora mas posso enviar 100 de uma vez". Recomendado para WhatsApp.
- Leaky bucket: taxa de saída fixa, sem burst. Bom para "1 msg a cada 800ms, sem exceção". Mais conservador.
- Sliding window: conta últimos N requests em janela rolante. Mais preciso mas custoso (precisa armazenar timestamps).
- Fixed window: simples, mas vulnerável a "quebra na borda" — 1k mensagens nas últimas 5s da janela + 1k nos primeiros 5s da próxima = 2k/10s.
Recomendação: token bucket para envio (permite ráfagas controladas), sliding window para análise de violações.
Warmup de número novo
Receita testada para subir um número de 0 a 5k msgs/dia em 30 dias sem ban:
const warmupSchedule = [
{ day: 1, maxPerHour: 5, total: 30 },
{ day: 2, maxPerHour: 10, total: 60 },
{ day: 3, maxPerHour: 15, total: 90 },
{ day: 7, maxPerHour: 50, total: 300 },
{ day: 14, maxPerHour: 150, total: 800 },
{ day: 21, maxPerHour: 400, total: 2000 },
{ day: 30, maxPerHour: 800, total: 5000 },
];
function getCurrentLimit(numberCreatedAt: Date): { perHour: number; perDay: number } {
const days = Math.floor((Date.now() - numberCreatedAt.getTime()) / (24 * 60 * 60 * 1000));
const slot = warmupSchedule.findLast(s => s.day <= days) ?? warmupSchedule[0];
return { perHour: slot.maxPerHour, perDay: slot.total };
}
Acima disso, deixe o número crescer organicamente. Aumentos abruptos (50 para 5000 num dia) acendem alerta de spam.
Casos práticos
Caso 1: SaaS de cobrança que perdia números
Antes de implementar token bucket, perdia 1-2 números/mês para banimento. Pico era no dia 1 (cobrança em massa). Após implementar throttle dinâmico (1k/hora baseline com redução para 300/hora se erro >5%), zero banimentos em 8 meses.
Caso 2: Loja virtual com 4 números operacionais
Distribui carga entre 4 números via round-robin com peso. Número mais antigo (2 anos) recebe 50% do volume, dois aquecidos (6+ meses) recebem 20% cada, número novo está em warmup recebendo 10%. Aumentou capacidade total sem queimar nenhum.
Caso 3: Bot de atendimento 24/7
Throttle por usuário também — limita 5 mensagens do bot para o mesmo cliente em 60 segundos. Evita loops onde bot responde a si mesmo (cenário comum quando integra com IA generativa). Implementou em sliding window per-user, custo Redis irrisório.
FAQ
Qual algoritmo usar?
Token bucket por instância (controla volume) + sliding window por destino (evita spam para o mesmo número) + warmup escalado (números novos). Os 3 juntos protegem 95% dos cenários.
Como sincronizo o bucket entre múltiplos workers?
Redis com Redlock (mostrado acima). Cada worker faz takeToken via lock distribuído — mesmo com 10 workers paralelos, o estado fica consistente.
O que faço quando recebo 429?
Leia o header Retry-After e respeite. Coloque a mensagem de volta na fila com delay. Não retry imediato — vai estourar de novo.
Como observo o rate limit em produção?
Exporte métricas Prometheus: rate_limit_tokens{instance="..."}, rate_limit_throttled_total, rate_limit_429_total. Crie alerta: tokens=0 por >5min indica que o limite está apertado para o volume — aumente capacity ou refill.
Número novo, qual o ideal nos primeiros 7 dias?
5-30 mensagens/dia, distribuídas (não 30 num minuto). Idealmente, mensagens reais para contatos reais que respondem — interação cria sinal positivo com o WhatsApp. Disparo em massa em número novo é receita de banimento garantido.
Próximo passo
Implemente os 3 layers (token bucket + dynamic + warmup) em uma tarde. Criar conta grátis e ative o monitoramento de rate limit no painel desde o dia 1.