1. Assuma que uma mensagem pode voltar#

Um worker conclui uma escrita e perde a conexão antes de confirmar a mensagem. A fila não tem como saber se o efeito aconteceu e entrega de novo. Esse caso basta para quebrar um consumidor que incrementa um contador sem identificar o evento. As filas Standard do SQS documentam entrega pelo menos uma vez; o consumidor precisa tolerar duplicatas.

Pense em idempotência como uma propriedade do efeito: repetir a mesma intenção deixa o estado de negócio igual ao de uma única execução. Isso não equivale a ignorar qualquer requisição parecida. Você precisa de uma identidade estável para a mesma operação e de uma política para a mesma chave com conteúdo diferente.

Para se aprofundar: Como escolher fila de mensagens: SQS, Cloudflare Queues ou QStash

Documentação: AWS: entrega pelo menos uma vez no SQS ↗

2. Atribua identidade antes de publicar#

O produtor precisa gerar e guardar o event_id junto com a operação de negócio. Repetir a publicação mantém esse identificador. O consumidor usa uma chave que inclua o nome dele ou a versão do efeito e o tenant verificado do evento; assim, dois consumidores diferentes podem processar legitimamente o mesmo evento.

Autentique o canal e valide o schema antes de confiar nesses campos. Guarde uma impressão digital (hash) do conteúdo relevante para detectar reutilização incorreta de chave. Decida por quanto tempo o registro de deduplicação vive de acordo com a janela real de retries, replays e retenção; apagá-lo cedo demais permite que um replay aplique o efeito de novo.

3. Amarre deduplicação e efeito local#

Quando o efeito está no mesmo banco de dados, uma constraint única e uma transação permitem amarrar a marca de processamento à escrita. Inserir a marca, fazer commit e só depois aplicar o efeito abre uma janela em que uma queda perde trabalho. Fazer primeiro o efeito fora da transação abre a janela inversa: duplicá-lo.

SQL
CREATE TABLE processed_events (
  tenant_id uuid NOT NULL,
  consumer text NOT NULL,
  event_id uuid NOT NULL,
  processed_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (tenant_id, consumer, event_id)
);
Schema mínimo ilustrativo. Numa implementação completa, guarde também o hash do payload e a sua política de retenção.
Pseudocódigo
begin transaction
insert dedup key on conflict do nothing returning event_id
if inserted:
  apply business change in this same database transaction
else:
  verify stored payload fingerprint matches this event
commit
ack message
Se qualquer operação falhar, faça rollback e não confirme a mensagem. A marca nova e o efeito precisam ser confirmados juntos.

O ON CONFLICT oferece o mecanismo para resolver uma colisão de unicidade. O seu código ainda precisa distinguir uma colisão válida de uma mensagem inconsistente e verificar se a pré-condição de negócio foi cumprida. Uma escrita que não afetou a linha esperada pode exigir erro e rollback, não sucesso silencioso.

Documentação: PostgreSQL: INSERT e ON CONFLICT ↗

4. Separe publicação e efeitos externos#

A transação anterior não inclui uma API externa. Se você envia um e-mail ou chama outro serviço antes do commit, um rollback local não desfaz esse efeito. Se fizer depois, uma queda pode impedir o envio. Projete esse limite com uma outbox transacional e, quando existir, uma chave de idempotência aceita pelo destino.

A outbox guarda a intenção de publicar junto com a mudança de negócio na mesma transação. Um relay a envia e registra o progresso. Esse relay também pode reenviar se cair entre publicar e registrar o sucesso; o consumidor continua precisando de deduplicação. A outbox resolve a perda entre banco e broker, mas não transforma todos os sistemas em uma transação única.

Documentação: AWS: padrão transactional outbox ↗

5. Faça retries com orçamento e dispersão#

Classifique as falhas: um timeout ou uma indisponibilidade temporária podem se resolver; um schema inválido não vai melhorar com repetição. Defina número de tentativas, tempo total e atraso máximo. Adicione jitter para que muitos workers não voltem a bater num serviço no mesmo instante. Respeite as indicações de espera do destino quando houver.

TypeScript
function retryDelayMs(attempt: number) {
  const base = 500;
  const cap = 30_000;
  const ceiling = Math.min(cap, base * 2 ** Math.min(attempt, 16));
  return Math.floor(Math.random() * ceiling);
}
Exemplo de full jitter; base e limite são valores ilustrativos. Quem chama precisa limitar tentativas e duração total.

Ajuste a visibilidade ou o mecanismo de lease ao tempo de processamento e estenda-o se o provedor permitir. Se expirar enquanto você trabalha, outro consumidor pode receber a mesma mensagem. Não use um atraso longo como substituto da idempotência.

6. Projete uma DLQ que você consiga operar#

Uma fila de mensagens com falha precisa de dono, alerta e procedimento de revisão. Guarde causa, tentativas e versão do consumidor sem despejar segredos nos logs. Reproduza a falha no ambiente autorizado, corrija a causa e reenvie um lote pequeno com a identidade original antes de uma recuperação em massa.

Ponto de quedaResultado esperado
Antes do commitEfeito e marca são revertidos; um retry pode processar.
Depois do commit, antes do ackO retry detecta a chave e não repete o efeito.
Payload inválidoÉ isolado com uma causa acionável, sem loop infinito.
Destino externo ambíguoConsulta-se ou reutiliza-se a identidade do destino; não se inventa sucesso.

Meça a idade da mensagem mais antiga, erros, backlog e tempo até a recuperação. O preço por milhão ajuda a estimar o gasto, mas não diz quanto tempo o seu time vai levar para consertar um evento travado. Essa capacidade operacional faz parte da escolha de uma fila.

Fontes e escopo

Documentação consultada em 25 de setembro de 2026. Os exemplos e critérios de decisão são propostas editoriais; adapte-os ao contrato da sua aplicação e valide-os no seu ambiente de testes autorizado.

Do design à decisão

Compare filas de mensagens

Confira preços, limites, condições e fontes de cada opção.

Abrir comparador