ResiliênciaAvançado9 min de leitura7 seções1984, Data aproximada

Garantias de entrega

At-most-once perde, at-least-once duplica, e exactly-once não existe entre sistemas distintos. Entender qual das três você tem — e por que a terceira é vendida sem ser verdade — é o que decide se o cliente vai ser cobrado duas vezes.

Em 10 segundos

Confirmar antes de processar perde; confirmar depois duplica. Não há terceira via — 'exactly-once' entre sistemas distintos é impossível, e o que os brokers vendem com esse nome vale só dentro do próprio broker. A saída prática é at-least-once com efeito idempotente: entrega repetida, efeito único.

Antes de ter nome

O debate 'exactly-once é mentira' voltou com força na era do Kafka (meados dos anos 2010), mas a impossibilidade já estava clara na teoria de RPC e no problema dos generais bizantinos.

  • #mensageria
  • #at-least-once
  • #exactly-once
  • #entrega
  • #resiliencia
Analogia

Os dois generais

Dois generais em morros opostos precisam atacar no mesmo horário e só podem se comunicar por mensageiros que atravessam o vale inimigo. O primeiro manda 'atacamos às 6h'. Mas ele não sabe se chegou — então o segundo confirma. Só que o segundo não sabe se a confirmação chegou. E assim por diante, para sempre. Nenhuma quantidade de mensagens produz certeza mútua. É um resultado provado, não uma limitação de tecnologia.

01Conceito

As três semânticas, e o que cada uma custa

At-most-once: confirma a mensagem antes de processar. Se o processo morrer no meio, ninguém reentrega — a mensagem se perdeu, e nada duplica.

At-least-once: confirma depois de processar. Se o processo morrer entre o efeito e a confirmação, a mensagem volta e o efeito acontece de novo.

Exactly-once: seria confirmar e processar como uma coisa só, atomicamente, através da rede. Não é possível.

A impossibilidade não é de engenharia, é matemática: é o problema dos dois generais. Entre dois sistemas que só se comunicam por um canal que pode falhar, não existe protocolo que produza acordo garantido em número finito de mensagens. Qualquer confirmação pode se perder, e a confirmação da confirmação também.

O que os brokers chamam de exactly-once é real, mas tem escopo. O Kafka consegue porque, num fluxo *ler-processar-escrever* inteiramente dentro do Kafka, o avanço do offset e a escrita do resultado entram na mesma transação do próprio Kafka — é um sistema fechado, não uma travessia de rede. No instante em que o efeito sai dali (seu banco, uma API de terceiro, um e-mail), a garantia acaba, e o problema volta a ser seu.

A escolha, portanto, é entre perder e duplicar. Para quase todo efeito de negócio — crédito de saldo, envio de nota, baixa de estoque — perder é inaceitável e duplicar é corrigível. Por isso at-least-once é o padrão de fato, e por isso Idempotência deixa de ser um refinamento e vira pré-requisito.

Sem o padrão × com o padrão

At-least-once com efeito idempotente

  • A chave do evento é registrada e o efeito aplicado na mesma transação
  • O processo morre entre o efeito e a confirmação
  • O broker reentrega, igual
  • A chave já existe: a reentrega não faz nada

A duplicação continua acontecendo — ela apenas deixou de ter consequência. É este o significado prático de 'exactly-once'.

Note que a diferença não está na entrega, que é idêntica nos dois casos, e sim no efeito. Não se elimina a reentrega; torna-se a reentrega inofensiva.
02Conceito

A chave é do evento, e a transação é uma só

Deduplicar exige duas decisões que costumam ser tomadas erradas: qual é a chave, e onde ela é gravada em relação ao efeito.

A chave precisa identificar o evento, não a tentativa — se cada reentrega gerar uma chave nova, a deduplicação nunca encontra nada.

Gravar a chave e aplicar o efeito precisam acontecer na mesma transação. Separados, existe uma janela: marcou e morreu antes de aplicar (perdeu o efeito para sempre, porque a reentrega vai achar a chave e desistir), ou aplicou e morreu antes de marcar (duplica na reentrega). A atomicidade entre os dois é o que faz o mecanismo funcionar.

Quando o efeito não está no mesmo banco — mandar e-mail, chamar API de terceiro — não há transação possível, e a deduplicação vira melhor esforço com uma janela de risco declarada. Nesses casos, o certo é empurrar a idempotência para o outro lado: usar a chave de idempotência que o provedor oferece (o Stripe faz isso) em vez de tentar garantir do lado de cá.

E a tabela de processados precisa de expurgo. Sem TTL, ela cresce para sempre e vira o gargalo que o consumo não tinha. A janela de retenção se define pelo tempo máximo em que uma reentrega ainda é plausível — normalmente dias, não anos.

03No mundo real

Como isso aparece na prática

O webhook de pagamento entregue duas vezes

Cenário

O provedor entrega o webhook de pagamento aprovado. O handler credita o saldo e leva 8 segundos para responder; o provedor considera timeout em 5 e reentrega. O saldo é creditado duas vezes.

Como o padrão entra

O id do evento do provedor vira a chave; a inserção na tabela de processados e o crédito acontecem na mesma transação. A reentrega passa a não fazer nada.

Trade-off: Aparece uma tabela nova, com escrita a cada evento e necessidade de expurgo. É custo real de armazenamento e manutenção, trocado pela impossibilidade de crédito duplicado.

A métrica que podia se perder

Cenário

Um coletor recebe milhões de amostras de telemetria por minuto. Deduplicar cada uma exigiria uma tabela de chaves maior que os próprios dados, e os painéis toleram bem uma amostra ausente.

Como o padrão entra

At-most-once assumido de propósito: confirma antes de processar, sem registro de deduplicação, com a perda documentada como decisão.

Trade-off: Contagens ficam levemente subestimadas em cenários de falha, o que inviabiliza usar essas métricas para cobrança. A decisão precisa estar escrita, ou alguém vai construir faturamento em cima delas.

Confiar no exactly-once do broker

Compila, roda, passa no code review. E quebra depois.

A configuração diz exactly-once, então o consumidor aplica o efeito direto, sem registro de deduplicação. A garantia existe — só que o escopo dela termina onde o seu efeito começa.

// "Configuramos exactly-once, entao nao precisa deduplicar."
consumidor.configurar({ entrega: "exactly-once" });

consumidor.escutar(async (msg) => {
  // Nenhuma verificacao: confia na garantia do broker.
  await creditarSaldo(msg.contaId, msg.valor);
});

// O que a garantia do broker realmente cobre:
//   produtor -> broker -> consumidor   (dentro do sistema dele)
//
// O que ela NAO cobre — e e onde mora o dinheiro:
//
//   1. creditarSaldo() roda e commita no banco
//   2. o processo morre ANTES de confirmar a mensagem
//   3. o broker, corretamente, reentrega
//   4. creditarSaldo() roda de novo
//
// O saldo foi creditado duas vezes, e o broker cumpriu o contrato:
// ele entregou uma vez para um consumidor que nao terminou.
// Efeito colateral fora do sistema do broker nao entra na garantia.

O que quebra

  • Ao reiniciar o podMensagens processadas cujo ACK não chegou antes do encerramento são reentregues, e o efeito é aplicado de novo.
  • No rebalanceamentoAo redistribuir partições entre consumidores, mensagens em voo voltam para outro consumidor e são processadas duas vezes.
  • Na conciliaçãoO saldo diverge de um jeito que não bate com nenhum evento registrado, porque houve dois efeitos para um evento só.

A saída: A garantia do broker cobre o caminho dentro do broker. Efeito colateral fora dele — banco, e-mail, API de terceiro — nunca esteve incluído. Assuma at-least-once e torne o efeito idempotente: chave do evento e efeito na mesma transação.

05Cuidado

Onde isto dá errado

  1. Acreditar no rótulo exactly-once

    A garantia é real dentro do sistema do broker e termina onde o seu efeito começa. Quem lê o rótulo e dispensa a deduplicação descobre isso num incidente de conciliação, meses depois.

  2. Chave da tentativa em vez do evento

    Se a chave for gerada no consumidor a cada recebimento, toda reentrega produz uma chave nova e a deduplicação nunca encontra nada. A chave precisa vir do evento e ser estável entre reentregas.

  3. Marcar e agir em transações separadas

    Marcar antes e morrer perde o efeito para sempre — a reentrega acha a chave e desiste. Agir antes e morrer duplica. Só a atomicidade entre os dois elimina a janela.

  4. Tabela de deduplicação sem expurgo

    Sem TTL ela cresce indefinidamente, e o índice que garantia a unicidade vira o gargalo do consumo. A retenção se dimensiona pelo tempo máximo em que uma reentrega ainda é plausível.

  5. Deduplicar por conteúdo

    Usar um hash do corpo como chave confunde dois eventos legítimos e idênticos — duas transferências do mesmo valor, para a mesma conta, no mesmo segundo — com uma reentrega. Só um identificador do evento distingue os dois casos.

  6. Confirmar antes de processar sem querer

    Muitos clientes confirmam automaticamente ao entregar a mensagem ao handler. Isso é at-most-once por acidente: qualquer falha no processamento perde o dado em silêncio, e ninguém escolheu isso.

06Implementação

Chave do evento e efeito na mesma transação

// A unica garantia que atravessa a rede com seguranca e at-least-once.
// "Exactly-once" util se constroi assim: entrega repetida + efeito unico.

async function consumir(msg: Mensagem) {
  // 1. A chave e do EVENTO, nao da tentativa: ela precisa ser a mesma
  //    em todas as reentregas da mesma mensagem.
  const chave = msg.id;

  // 2. Marcar e agir na MESMA transacao. Se estiverem separados,
  //    existe uma janela em que um aconteceu e o outro nao.
  await db.transacao(async (tx) => {
    const novo = await tx.processados.inserirSeAusente(chave);
    if (!novo) return;              // ja processamos: ignora em silencio
    await creditarSaldo(tx, msg.contaId, msg.valor);
  });

  await msg.confirmar();
}

// Por que nao da para ter exactly-once de verdade:
// depois de escrever no banco e antes de confirmar a mensagem,
// o processo pode morrer. Nenhuma ordem das duas operacoes elimina
// essa janela — so a idempotencia a torna inofensiva.
07Decisão

Quando usar × quando evitar

Use quando

3
  • Em todo consumo de fila, webhook recebido ou efeito disparado por rede.
  • Ao integrar com terceiros, que entregam at-least-once na prática.
  • Ao decidir explicitamente se um dado pode ser perdido.

Evite quando

2
  • Deduplicar quando o efeito já é naturalmente idempotente (um UPDATE que fixa valor absoluto).
  • At-most-once para qualquer coisa que envolva dinheiro ou estado do cliente.

Testar o que ficou

6 armadilhas de Garantias de entrega viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • SQS padrão × FIFOA fila padrão é at-least-once assumido; a FIFO oferece deduplicação numa janela de minutos, não para sempre.
  • enable.idempotence do KafkaResolve duplicação do produtor para o broker — dentro do Kafka. O efeito no seu banco continua sendo seu problema.
  • O ACK do TCPO protocolo retransmite quando não recebe confirmação, o que é at-least-once no nível do segmento.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Entre sistemas, prepare-se para at-least-once.
// Idempotência no consumidor fecha o ciclo.

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • At-least-once obriga todo consumidor a ser idempotente para tolerar duplicatas
  • Deduplicar de verdade exige guardar os ids já vistos, com estado e janela de retenção

Não vale se: a perda ocasional de uma mensagem é aceitável para o caso — aí at-most-once sai mais barato que qualquer garantia forte.

Onde isto aparece

Continue por aqui