ZAP-API
PreçosCasos de UsoBlogDocsLogin
Começar grátis
  1. Blog
  2. Outbox pattern no WhatsApp: garantia de entrega sem perda de mensagem
Arquitetura

Outbox pattern no WhatsApp: garantia de entrega sem perda de mensagem

Como implementar o Outbox Pattern para garantir que mensagens WhatsApp sejam entregues mesmo quando o gateway está fora, com retry automático e zero perda.

13 de maio de 2026·14 min de leitura·Equipe Editorial ZAP API

Cenário comum: sua aplicação faz checkout, salva o pedido no banco, e depois chama a API para mandar WhatsApp ao cliente. Tudo funciona em desenvolvimento. Em produção, um dia o gateway WhatsApp está fora 30 segundos no momento exato em que sua aplicação faz o disparo. Resultado: pedido salvo, mensagem perdida, cliente sem aviso, ticket aberto no SAC.

O Outbox Pattern resolve isso de forma definitiva. Persiste a intenção de envio em uma tabela ANTES de chamar o gateway, e um worker assíncrono consome a tabela com retry automático. Se o gateway estiver fora, o worker tenta de novo até conseguir. Zero mensagem perdida, zero acoplamento entre fluxo de negócio e disponibilidade do gateway.

O problema clássico

// Padrão fire-and-forget (PROBLEMÁTICO em produção)
async function processarPedido(pedido) {
  await db.salvarPedido(pedido);
  await whatsappAPI.enviar({          // ← se falhar aqui...
    to: pedido.telefone,
    text: `Pedido #${pedido.id} confirmado`,
  });
}

Quatro modos de falha possíveis:

  1. Network timeout temporário do gateway
  2. Gateway WhatsApp passando por incidente
  3. Sua aplicação cair entre salvarPedido e enviar
  4. Rate limit do WhatsApp (você passou de algum threshold)

Em todos os 4 casos, a mensagem se perde silenciosamente.

O Outbox Pattern

Ideia central: salvar a intenção de envio no banco de dados ANTES de chamar o gateway. Worker separado consome o banco e faz o envio com retry. A operação de "salvar pedido" e "agendar mensagem" vira uma transação atômica.

Fluxo

Request HTTP                     Worker (loop)
     │                                 │
     ▼                                 ▼
┌─────────┐  transação  ┌──────────────┐
│ Pedido  │───────────▶│ outbox_jobs  │
│ DB      │             │ status=PENDING│
└─────────┘             └──────────────┘
                                │
                                ▼
                       ┌──────────────────┐
                       │ ZAP API HTTP     │
                       └──────────────────┘
                                │
                  sucesso       │       falha
                    ▼           │         ▼
            ┌──────────────┐    │   ┌──────────────────┐
            │ status=SENT  │    │   │ attempt+=1       │
            └──────────────┘    │   │ next_retry_at=+5m│
                                │   └──────────────────┘
                                │           │
                                │   após 5 falhas
                                │           ▼
                                │   ┌──────────────┐
                                │   │ status=DEAD  │
                                │   └──────────────┘

Schema da tabela outbox_jobs

CREATE TABLE outbox_jobs (
  id              BIGSERIAL PRIMARY KEY,
  instance_id     VARCHAR(64) NOT NULL,
  payload         JSONB NOT NULL,
  status          VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  attempts        INT NOT NULL DEFAULT 0,
  max_attempts    INT NOT NULL DEFAULT 5,
  next_retry_at   TIMESTAMP NOT NULL DEFAULT NOW(),
  last_error      TEXT,
  idempotency_key VARCHAR(128) UNIQUE,
  created_at      TIMESTAMP NOT NULL DEFAULT NOW(),
  sent_at         TIMESTAMP NULL
);

CREATE INDEX idx_outbox_pending ON outbox_jobs(next_retry_at)
  WHERE status = 'PENDING';

CREATE INDEX idx_outbox_dead ON outbox_jobs(status)
  WHERE status = 'DEAD';

Código: persistir intenção (no fluxo HTTP)

// rota POST /pedido
import { Pool } from "pg";
const db = new Pool({ connectionString: process.env.DATABASE_URL });

async function criarPedido(req, res) {
  const { telefone, valor, instanceId } = req.body;
  const pedidoId = generateId();

  // Tudo na mesma transação
  const tx = await db.connect();
  try {
    await tx.query("BEGIN");
    await tx.query("INSERT INTO pedidos (id, telefone, valor) VALUES ($1, $2, $3)",
      [pedidoId, telefone, valor]);

    await tx.query(`
      INSERT INTO outbox_jobs (instance_id, payload, idempotency_key)
      VALUES ($1, $2, $3)
    `, [
      instanceId,
      JSON.stringify({
        type: "text",
        to: telefone,
        text: `Pedido #${pedidoId} confirmado.`,
      }),
      `pedido-${pedidoId}-confirmacao`,
    ]);

    await tx.query("COMMIT");
    res.status(201).json({ pedidoId });
  } catch (err) {
    await tx.query("ROLLBACK");
    throw err;
  } finally {
    tx.release();
  }
}

O ponto-chave: se o gateway WhatsApp está fora nesse momento, não importa. O job está persistido. Vai sair quando puder.

Código: worker que processa

// outbox-worker.js
import axios from "axios";
import { Pool } from "pg";

const db = new Pool({ connectionString: process.env.DATABASE_URL });
const ZAP = axios.create({
  baseURL: "https://api.zap-api.tech/v1",
  headers: { Authorization: `Bearer ${process.env.ZAP_TOKEN}` },
});

const BACKOFF_MIN = [60, 300, 1800, 7200, 86400]; // 1m, 5m, 30m, 2h, 24h

async function loop() {
  while (true) {
    try {
      await processarBatch();
    } catch (err) {
      console.error("Erro no loop:", err);
    }
    await new Promise((r) => setTimeout(r, 5000));
  }
}

async function processarBatch() {
  // FOR UPDATE SKIP LOCKED permite múltiplos workers paralelos
  const { rows: jobs } = await db.query(`
    SELECT id, instance_id, payload, attempts, max_attempts
    FROM outbox_jobs
    WHERE status = 'PENDING'
      AND next_retry_at <= NOW()
    ORDER BY next_retry_at
    LIMIT 50
    FOR UPDATE SKIP LOCKED
  `);

  for (const job of jobs) {
    try {
      await ZAP.post(`/instances/${job.instance_id}/messages`, job.payload);

      await db.query(
        "UPDATE outbox_jobs SET status = 'SENT', sent_at = NOW() WHERE id = $1",
        [job.id]
      );
    } catch (err) {
      const novosAttempts = job.attempts + 1;
      const novoStatus = novosAttempts >= job.max_attempts ? "DEAD" : "PENDING";
      const idxBackoff = Math.min(novosAttempts - 1, BACKOFF_MIN.length - 1);
      const proximaTentativa = `NOW() + INTERVAL '${BACKOFF_MIN[idxBackoff]} seconds'`;

      await db.query(`
        UPDATE outbox_jobs SET
          status = $1,
          attempts = $2,
          next_retry_at = ${proximaTentativa},
          last_error = $3
        WHERE id = $4
      `, [novoStatus, novosAttempts, err.message, job.id]);
    }
  }
}

loop();

Como a ZAP API implementa Outbox internamente

A ZAP API já tem outbox interno transparente para você. Quando você chama POST /v1/instances/:id/messages, internamente:

  1. Validamos payload
  2. Persistimos em fila interna (Redis-backed)
  3. Worker interno consome e despacha para o gateway WhatsApp
  4. Retry automático com backoff exponencial em falhas transientes
  5. Webhook de status (sent/delivered/read/failed) dispara para sua URL configurada

Ou seja: a ZAP API já protege contra o cenário "gateway WhatsApp fora 30s". O outbox próprio que você implementa em cima protege contra "ZAP API fora 30s" — defesa em profundidade.

Quando usar outbox próprio vs confiar na ZAP API

Suficiente confiar na ZAP API (sem outbox próprio)

  • Aplicação tolera latência de 0-30s entre evento e mensagem
  • Volume baixo/médio (até alguns milhares por dia)
  • Não tem requisito formal de "garantia at-least-once"

Outbox próprio recomendado

  • Volume alto (10mil+/dia) ou pico imprevisível
  • Mensagem é parte de fluxo crítico (financeiro, agendamento médico, evento de uma vez)
  • Auditoria e rastreabilidade são requisitos
  • Múltiplas APIs (não só WhatsApp), e você quer um padrão único

Outbox vs fila clássica (BullMQ/Redis)

CaracterísticaOutbox (DB)BullMQ (Redis)
Atomicidade com dado de negócioSim (mesma transação)Não (eventual)
Performance pura~1k/s10k+/s
PersistênciaForteForte (com AOF)
AuditoriaTrivial (SQL)Limitada
Múltiplos workersFOR UPDATE SKIP LOCKEDNativo
Quando usarVolume médio + atomicidadeVolume alto + perf

Casos práticos

Caso 1: E-commerce em Black Friday

Loja com 50k pedidos no dia. Pico de 200/min nas primeiras 2 horas. Outbox próprio em PostgreSQL com 4 workers paralelos consumindo. Zero mensagem perdida mesmo com gateway WhatsApp tendo 2 incidentes durante o evento.

Caso 2: Agendamento médico

Clínica com 800 consultas/dia. Cada consulta gera 3 mensagens (confirmação imediata, lembrete D-1, lembrete H-2). Outbox interno da ZAP API foi suficiente. Worker próprio teria sido sobre-engenharia.

Caso 3: Plataforma de cursos

Disparos massivos em momentos específicos (lançamento de módulo, lembrete de prazo): 20k mensagens em 5 minutos. BullMQ + workers se mostrou melhor que outbox em DB, porque o pico breve não justificava transação atômica com dado de negócio.

FAQ

Diferença entre outbox e fila (BullMQ)?

Outbox vive na mesma transação que sua escrita de negócio (sem chance de inconsistência). Fila é externa (Redis), sem garantia transacional com seu DB. Para a maioria dos casos críticos, outbox é mais seguro.

Como monitorar jobs travados?

Query simples: SELECT count(*) FROM outbox_jobs WHERE status = 'PENDING' AND created_at < NOW() - INTERVAL '10 minutes'. Se passar de 100, alerta. Para jobs DEAD, alerta sempre que houver — significa falha de retry esgotada que precisa de intervenção humana.

Como fazer worker idempotente?

Use idempotency_key único (campo na tabela). Antes de processar, faça SELECT — se já está SENT, pule. Garantia: mesmo job processado 2x não duplica envio.

Posso ter múltiplos workers paralelos?

Sim, é justamente a vantagem. Use FOR UPDATE SKIP LOCKED no SELECT — cada worker pega jobs diferentes sem conflitar. Em produção, 4-8 workers atendem volume médio.

Como reprocessar mensagem em status DEAD?

Manualmente via SQL: UPDATE outbox_jobs SET status = 'PENDING', attempts = 0, next_retry_at = NOW() WHERE id = ?. Antes, investigue: se a mensagem morreu por payload inválido, fix o payload primeiro.

Próximo passo

Implemente outbox próprio em algumas horas com o exemplo acima. Criar conta grátis e proteja seu fluxo desde o primeiro dia.

Experimente a ZAP API gratuitamente

7 dias de trial sem precisar de cartão. A partir de R$29/mês*.

Criar instância grátis
EE
Equipe Editorial ZAP APIRevisão técnica

Desenvolvedores e especialistas em integrações WhatsApp. Todo conteúdo passa por revisão técnica para garantir precisão e aplicabilidade.

Ver perfil completoDocumentaçãoTrial grátis

Leia também

Arquitetura · 30 de abr. de 2026 · 14 min

Como escalar WhatsApp para 100 mil mensagens/dia: arquitetura, fila e custo real

Limites por instância, cálculo de quantas você precisa, outbox pattern, circuit breaker, warmup pool e 4 métricas vitais. Custo unitário de R$ 0,0009/msg.

Arquitetura · 30 de mai. de 2026 · 13 min

Mensagem idempotente no WhatsApp: nunca envie a mesma mensagem duas vezes

Como implementar idempotência em disparos WhatsApp para evitar mensagens duplicadas em retries, falhas de rede e race conditions em sistemas distribuídos.

Arquitetura · 26 de mai. de 2026 · 14 min

Multi-instância WhatsApp: arquitetura para escalar sem banimento

Como distribuir envios entre múltiplas instâncias WhatsApp para escalar volume, isolar clientes e proteger números críticos de banimento por sobrecarga.

Arquitetura · 15 de mai. de 2026 · 13 min

Circuit breaker em WhatsApp API: proteja sua instância de sobrecarga

Implemente circuit breaker para sua integração WhatsApp: evite cascata de falhas, proteja sua instância de rate limiting e garanta degradação graciosa.

Tópicos:Chatbots com IAE-commerceAPI WhatsApp

Explore também

Casos de usoWhatsApp API por segmentoComparativoZAP API vs alternativasPreçosPlanos e o que está inclusoGlossárioTermos técnicos de WhatsApp API
ZAP-API

API REST para WhatsApp com webhooks assinados, Meta Pixel/CAPI e compliance LGPD. Sem aprovação da Meta.

Status operacional🇧🇷 Feito no Brasil

Produto

  • Preços
  • Casos de uso
  • Comparativo
  • Trial grátis
  • Dashboard

Recursos

  • Documentação
  • Blog
  • Glossário
  • RSS Feed

Empresa

  • Sobre
  • Imprensa
  • Termos de uso
  • Privacidade
  • Criar conta
  • Login

Contato

  • [email protected]
  • [email protected]
  • Resposta em até 24h úteis
© 2026 ZAP-API — Todos os direitos reservados·CNPJ 42.130.949/0001-56·Termos·Privacidade

Desenvolvido por PreviusIA