Idempotência em Sistemas Distribuídos que Funciona de Verdade

Evite efeitos colaterais duplicados

Conteúdo da página

Idempotência em sistemas distribuídos é a propriedade que te protege quando a rede mente, as filas tentam novamente, o cliente entra em pânico e o operador aciona o replay. Em sistemas de produção, a entrega duplicada é normal. Efeitos coliduais duplicados são o bug.

O HTTP define um método idempotente como aquele onde múltiplas solicitações idênticas têm o mesmo efeito pretendido no servidor que uma única solicitação. É por isso que PUT, DELETE e métodos seguros são idempotentes nas semânticas do protocolo e podem ser tentados novamente automaticamente após uma falha de comunicação.

fluxo de mensagem de integração: idempotência

Essa definição é útil, mas não é suficiente. Em arquiteturas reais, a idempotência não é uma resposta trivial do HTTP. É uma garantia de negócio. Se um cliente clica em “pagar” uma vez, você não pode cobrar duas vezes porque ocorreu um tempo limite entre o commit e a resposta. Se um worker atualiza o inventário e falha antes de confirmar (ack) a mensagem, você não pode decrementar o estoque duas vezes porque o broker reentregou a mesma mensagem. Essa é a barra.

O erro que vejo repetidamente é tratar a idempotência como um recurso de transporte em vez de uma propriedade do sistema. A deduplicação de filas, os verbos HTTP e as tentativas do cliente ajudam, mas nenhum deles resgata um design que permita que a mesma intenção de negócio crie um segundo efeito colateral. Se você quiser uma visão mais ampla de como essas decisões de integração se encaixam nos limites dos serviços e nas compensações de persistência, comece com Arquitetura de Aplicativos em Produção: Padrões de Integração, Design de Código e Acesso a Dados.

De onde vêm os duplicados na produção

Os duplicados não aparecem porque as equipes são descuidadas. Eles aparecem porque sistemas distribuídos tentam novamente, reordenam e reproduzem.

Um cliente pode enviar uma solicitação de criação, o servidor pode confirmá-la, e a resposta ainda pode desaparecer no cabo. É exatamente por isso que o HTTP distingue métodos idempotentes e por que APIs de pagamento, como Stripe e PayPal, expõem mecanismos explícitos de idempotência para métodos inseguros, como POST.

Os brokers de mensagens tornam o problema ainda mais óbvio. A entrega “no mínimo uma vez” (at-least-once) significa que um consumidor pode ser invocado repetidamente para a mesma mensagem, e um manipulador pode atualizar o banco de dados com sucesso, mas falhar antes da confirmação, fazendo com que o broker entregue a mesma mensagem novamente.

Webhooks não são diferentes. O GitHub diz que as entregas de webhooks podem chegar fora de ordem, as entregas falhas não são reentregadas automaticamente e cada entrega carrega um GUID único X-GitHub-Delivery que você deve usar ao se proteger contra replay. Para uma visão prática da arquitetura sobre endpoints de chat como limites de interação, veja Plataformas de Chat como Interfaces de Sistema em Sistemas Modernos.

Mesmo sistemas que anunciam garantências mais fortes ainda deixam trabalho para você fazer. O Kafka pode evitar entradas duplicadas nos logs do Kafka com produtores idempotentes e pode fornecer entrega exatamente uma vez para fluxos de leitura-processamento-escrita que permanecem dentro do Kafka com transações e consumidores read_committed. Mas os próprios documentos de design do Kafka são claros de que sistemas externos ainda exigem coordenação com offsets e saídas. A entrega exatamente uma vez do Google Cloud Pub/Sub é limitada a assinaturas pull, dentro de uma região da nuvem e ainda exige que os clientes rastreiem o progresso do processamento até que a confirmação seja bem-sucedida.

Minha conclusão opinativa é simples. Suponha que o transporte tentará novamente. Suponha que os operadores retransmitirão. Suponha que os webhooks chegarão atrasados. Projete o caminho de gravação para que uma intenção repetida não possa criar um segundo efeito de negócio. O design de erros está intimamente relacionado: como os erros são agrupados, traduzidos e classificados como retryable (tentáveis novamente) versus non-retryable (não tentáveis) faz parte da mesma disciplina de limite — Arquitetura de Manipulação de Erros em Go: Limites e Padrões cobre a classificação de erros retryable, tradução de limites e os padrões sentinela que permitem que a lógica de retry tome decisões sólidas. Quando as tentativas continuam atingindo uma dependência não saudável, um breaker de circuito no limite de integração falha rapidamente antes que as tempestades de retry amplifiquem o trabalho duplicado.

O contrato de API em que realmente confio

Como as chaves de idempotência previnem solicitações duplicadas de API

O único contrato de API em que confio para operações mutantes é a intenção fornecida pelo chamador mais a persistência no lado do servidor.

A AWS recomenda um identificador de solicitação fornecido pelo chamador e alerta que o serviço deve registrar atomicamente o token de idempotência junto com o trabalho mutante. O Stripe armazena o primeiro código de status e o corpo da resposta para uma chave, compara os parâmetros posteriores com a solicitação original e retorna o mesmo resultado para tentativas repetidas. O PayPal usa PayPal-Request-Id nas APIs POST suportadas e retorna o status mais recente para a solicitação anterior com esse mesmo cabeçalho.

Isso leva a um contrato prático:

  1. O cliente gera uma chave de idempotência para uma operação de negócio.
  2. O servidor escopariza essa chave por inquilino (tenant) e nome da operação.
  3. O servidor armazena um hash da solicitação para que a mesma chave não possa ser reutilizada para um payload diferente.
  4. O servidor registra o estado, como pending (pendente), completed (concluído) ou failed (falhou).
  5. Tentativas repetidas com a mesma chave retornam o resultado armazenado ou um ponteiro estável para ele.
  6. Tentativas repetidas com a mesma chave e um payload diferente falham ruidosamente.

Existe um rascunho de cabeçalho IETF Idempotency-Key, mas, em 2026-05-09, ainda está listado no IETF Datatracker como um Internet-Draft expirado, em vez de um RFC publicado. Na prática, o nome do cabeçalho ainda é amplamente útil como uma convenção de fato, mas você deve documentar o contrato em sua própria API em vez de fingir que o padrão está concluído.

O que a chave deve representar? Intenção. Não uma tentativa HTTP. Não uma conexão TCP. Não um contador de retry. Se o usuário quer dizer “criar o pedido 123 uma única vez”, cada retry para esse mesmo comando deve reutilizar a mesma chave. Se o usuário quer dizer “fazer um segundo pedido”, isso deve usar uma chave diferente.

Um ID de solicitação é para rastreamento. Uma chave de idempotência é para correção. Se você confundir esses conceitos, seus painéis parecerão organizados enquanto seu dinheiro se move duas vezes.

Por que PUT não é suficiente

Não, o HTTP PUT não é suficiente para tornar uma operação idempotente.

Sim, o RFC 9110 dá ao PUT semânticas idempotentes. Mas se o seu manipulador PUT emitir um novo evento downstream, enviar um e-mail em cada retry ou cobrar um provedor externo novamente, então sua implementação violou o contrato de negócio, mesmo que o nome da rota pareça respeitável.

A escolha do verbo ajuda os clientes a entenderem a intenção. Ela não implementa a intenção para você.

Use PUT quando o modelo de recurso realmente se encaixar em um estilo de operação de substituição completa ou upsert. Use POST quando estiver criando comandos ou ações. Mas, para qualquer mutação que possa ser tentada novamente através de limites de rede, documente um contrato explícito de idempotência. Se suas ações mutantes forem acionadas a partir de fluxos de trabalho de chat, o mesmo contrato se aplica em Padrões de Integração do Slack para Alertas e Fluxos de Trabalho e Padrão de Integração do Discord para Alertas e Laços de Controle. Efeitos colaterais ocultos são onde a arquitetura morre.

Por quanto tempo uma chave de idempotência deve ser armazenada

Mais tempo do que sua equipe de transporte deseja.

O Stripe diz que as chaves podem ser podadas após pelo menos 24 horas. O PayPal diz que a retenção é específica da API e fornece exemplos que podem durar até 45 dias. O Amazon SQS FIFO deduplica apenas dentro de uma janela de 5 minutos. O GitHub mantém as entregas recentes por 3 dias para reentrega manual. Esses números são amplamente diferentes porque o período de retenção certo é uma decisão de negócio, não um padrão de protocolo.

Se você mantiver chaves apenas por cinco minutos porque sua fila faz isso, você não está projetando idempotência. Está copiando uma limitação de transporte para sua camada de negócio.

Mantenha os registros de idempotência por pelo menos o máximo dessas janelas:

  • horizonte de retry do cliente
  • horizonte de redrive da fila
  • horizonte de replay de webhook
  • horizonte de replay do operador
  • horizonte de liquidação ou compensação para operações que movem dinheiro

Para pagamentos, reservas e provisionamento, isso geralmente significa horas ou dias, não minutos.

A AWS também destaca dois anti-padrões com os quais concordo plenamente. Não use timestamps como chave, porque o skew do relógio e as colisões os tornam pouco confiáveis. Não armazene cegamente todos os payloads de solicitação como registro de deduplicação para cada solicitação, porque isso prejudica o desempenho e a escalabilidade. Armazene um hash de solicitação normalizado mais o estado mínimo de resposta necessário para replay seguro. Se você precisar reproduzir o primeiro byte da resposta byte por byte, armazene o corpo de resposta canônico, como o Stripe faz.

Os padrões de banco de dados que tornam a idempotência real

A idempotência torna-se real quando a camada de persistência pode vencer uma corrida exatamente uma vez.

O PostgreSQL oferece dois primitivos críticos aqui. Restrições únicas garantem a unicidade em uma ou mais colunas, e INSERT ... ON CONFLICT permite que você defina uma ação alternativa em vez de falhar em uma violação de unicidade. O PostgreSQL também documenta que ON CONFLICT DO UPDATE garante um resultado atômico de inserção-atualização sob concorrência.

Isso significa que sua camada de idempotência deve geralmente começar com uma tabela como esta:

create table api_idempotency (
    tenant_id text not null,
    operation text not null,
    idempotency_key text not null,
    request_hash text not null,
    state text not null,
    status_code integer,
    response_body jsonb,
    resource_type text,
    resource_id text,
    created_at timestamptz not null default now(),
    expires_at timestamptz not null,
    primary key (tenant_id, operation, idempotency_key)
);

E o fluxo de manipulação deve parecer com isto:

begin transaction

try insert (tenant_id, operation, idempotency_key, request_hash, state='pending')
on conflict do nothing

load row for (tenant_id, operation, idempotency_key) for update

if row.request_hash != incoming_request_hash
    fail with conflict or validation error

if row.state = 'completed'
    return stored response

if row.state = 'pending' and row was created by another live request
    either wait briefly, or fail fast with a retryable response

perform local business mutation

store stable result in idempotency row
set state = 'completed'

commit
return result

A parte importante não é a sintaxe. A parte importante é a atomicidade. Registrar a chave e realizar a mutação devem ter sucesso ou falhar juntos. A AWS diz isso explicitamente para idempotência de API, e a mesma regra se aplica em serviços baseados em SQL.

Não faça uma sequência ingênua de verificar-agir, como “selecionar chave; se ausente, então inserir pedido”. Sob concorrência, duas solicitações podem passar pela verificação e ambas criar o efeito colateral. Uma restrição única não é opcional. É o mecanismo que transforma sua arquitetura de um folclore otimista em algo que você pode provar sob carga.

Aqui está a regra que uso em revisões. Se a decisão de deduplicação não for protegida pelo mesmo limite transacional que a mutação, você não tem idempotência. Você tem esperança.

Mensagens, eventos e webhooks precisam de seu próprio limite

Como os consumidores manipulam eventos e mensagens duplicadas

Para consumidores de mensagens, o padrão clássico ainda é o correto. Registre os IDs de mensagem processados na mesma transação de banco de dados que a atualização de negócio. Chris Richardson descreve a abordagem da tabela PROCESSED_MESSAGES diretamente, usando uma chave primária no assinante e ID da mensagem para que os duplicados falhem limpidamente e possam ser ignorados.

Muitas equipes chamam esse repositório explícito processed_messages de tabela de entrada (inbox). O rótulo importa menos do que a regra. O receptor deve persistir a prova de que já manipulou a mensagem antes que um retry possa seguramente não fazer nada.

Uma forma mínima se parece com isto:

create table processed_messages (
    subscriber_id text not null,
    message_id text not null,
    processed_at timestamptz not null default now(),
    primary key (subscriber_id, message_id)
);

E o fluxo do consumidor é tão rigoroso quanto o fluxo HTTP:

begin transaction

insert into processed_messages (subscriber_id, message_id)
values (?, ?)
on conflict do nothing

if no row inserted
    rollback
    ack and ignore duplicate

apply business mutation

commit
ack message

Esse padrão é entediante. Bom. A idempotência deve ser entediante.

Também é geralmente melhor do que tentar depender de termos de marketing de brokers. O suporte exatamente uma vez do Kafka é excelente quando você permanece dentro do próprio modelo transacional do Kafka, mas os documentos do Kafka ainda alertam que destinos externos precisam de cooperação. O SQS FIFO reduz apenas o envio duplicado dentro de sua janela de deduplicação de 5 minutos. O Pub/Sub exatamente uma vez ainda espera que o assinante rastreie o progresso e evite trabalho duplicado quando as confirmações falham.

Exatamente uma vez geralmente é uma otimização local. Efeitos colaterais idempotentes são a garantia do sistema.

Pare a deduplicação com o padrão outbox

Se o seu serviço atualiza o estado local e também publica um evento, o consumo idempotente sozinho não é suficiente. Você também precisa de uma maneira segura de obter o evento após o commit da transação local.

É por isso que o padrão transactional outbox importa. Chris Richardson descreve a ideia básica como escrever o evento em uma tabela outbox na mesma transação que a atualização de negócio e, em seguida, publicá-lo assincronamente. O Debezium diz que o padrão outbox evita inconsistências entre o estado interno de um serviço e os eventos consumidos por outros serviços. O NServiceBus vai além e mostra como o processamento do outbox deduplica mensagens recebidas e evita registros zombies e mensagens fantasmas.

Esta é a arquitetura que recomendo para serviços que possuem dados e publicam eventos de integração:

  1. Valide e persista o comando sob uma chave de idempotência.
  2. Escreva o estado de negócio e o evento do outbox em uma transação local única.
  3. Deixe o CDC ou um despachante do outbox publicar o evento.
  4. Faça os consumidores downstream também serem idempotentes.

O Outbox não remove a necessidade de consumidores idempotentes. Remove a necessidade de fingir que um commit de banco de dados e uma publicação de broker podem ser uma transação distribuída mágica quando geralmente não podem.

Webhooks são apenas mensagens com melhor branding

Trate webhooks entrantes exatamente como mensagens de uma borda de rede não confiável.

O GitHub documenta que as entregas podem chegar fora de ordem, recomenda usar X-Hub-Signature-256 para verificar a autenticidade e fornece X-GitHub-Delivery como o identificador de entrega único. Ele também observa que as reentregas reutilizam o mesmo ID de entrega.

Então a arquitetura é direta:

  • verifique a assinatura primeiro
  • use o GUID de entrega como a chave de deduplicação
  • persista o recibo antes dos efeitos colaterais
  • faça os manipuladores serem conscientes da ordem em vez de assumir a ordem de chegada
  • enfileire o trabalho pesado e retorne rapidamente

Se o seu manipulador de webhook escrever diretamente nas tabelas de negócio antes de registrar o recibo, não está pronto para produção. É apenas mais rápido ao cometer erros duplicados.

Sagas e motores de fluxo de trabalho ainda precisam de idempotência

Sagas e motores de fluxo de trabalho duráveis não eliminam o problema. Eles o tornam visível.

A Temporal recomenda escrever Atividades para serem idempotentes porque as Atividades podem ser tentadas novamente após falhas ou tempos limite. Seus documentos até destacam o caso de borda onde um worker conclui um efeito colateral externo com sucesso, mas falha antes de relatar a conclusão, o que faz com que a Atividade seja executada novamente. A Temporal também sugere usar uma combinação do Workflow Run ID e Activity ID como uma chave de idempotência estável ao chamar serviços downstream. Se você estiver aplicando isso na orquestração de serviços, Microsserviços Go para Orquestração de IA/ML cobre as compensações mais amplas do fluxo de trabalho.

Esse é exatamente o modelo mental certo. Um motor de fluxo de trabalho pode preservar o histórico de execução e coordenar tentativas. Ele não pode reverter uma cobrança de cartão ou desenviar um e-mail a menos que sua aplicação lhe dê etapas idempotentes e compensações idempotentes.

O mesmo se aplica às sagas. A própria orientação de saga da Temporal descreve ações de compensação que são executadas quando uma etapa falha. Essas compensações também devem ser idempotentes. Se “reembolsar pagamento” for executado duas vezes, você pode ter resolvido o bug original criando um novo.

Minha regra aqui é brutal e simples. Cada Atividade, cada manipulador de comando e toda compensação que toca o mundo externo deve ser naturalmente idempotente ou carregar uma chave de idempotência real para o sistema downstream.

Como testar a idempotência antes da produção

A maioria das equipes testa caminhos felizes e então age surpresa quando as tentativas repetidas acontecem. Isso não é suficiente. Para equipes Go, Testando Código Concurrente em Go com testing/synctest cobre como escrever testes rápidos e determinísticos para loops de retry e comportamento de deadline de contexto sem dormir através de atrasos artificiais.

Você deve ter testes automatizados para pelo menos estes casos:

  • o servidor confirma a mutação, mas a resposta nunca chega ao cliente
  • duas solicitações idênticas competem com a mesma chave de idempotência
  • a mesma chave é reutilizada com um payload diferente
  • um consumidor confirma seu trabalho de banco de dados e falha antes do ack
  • um webhook é retransmitido com o mesmo ID de entrega
  • um despachante do outbox publica o mesmo evento mais de uma vez
  • uma Atividade de fluxo de trabalho conclui a chamada externa e falha antes que a conclusão seja reportada
  • um registro de idempotência expira e um retry tardio genuíno chega

A AWS recomenda explicitamente suites de teste abrangentes que incluam solicitações bem-sucedidas, solicitações falhas e solicitações duplicadas. Esse conselho é pedestre e absolutamente correto.

Eu adicionaria mais uma drill de falha. Verifique se a resposta retransmitida é semanticamente equivalente ao primeiro resultado. A AWS discute retries de chegada tardia e argumenta por respostas que preservam o significado original mesmo após o estado subjacente ter mudado. Essa é a diferença entre “nenhum efeito colateral extra aconteceu” e “o chamador ainda tem um contrato consistente.”

Regras opinativas que salvam sistemas reais

Aqui estão as regras que eu aplicaria em uma revisão de arquitetura.

Primeiro, as chaves de idempotência pertencem à intenção de negócio, não às tentativas de transporte.

Segundo, escoparize cada chave por inquilino (tenant) e operação. Espaços de chave globais são como solicitações não relacionadas colidem.

Terceiro, persista a decisão de deduplicação atomicamente com a mutação. Se isso não for verdade, o design está errado.

Quarto, rejeite retries de mesma-chave-diferente-payload. O Stripe e a AWS fazem isso por um bom motivo.

Quinto, mantenha as chaves por todo o horizonte de replay do processo de negócio, não apenas pela janela de fila mais curta.

Sexto, pare produtores com um outbox e consumidores com rastreamento de ID de mensagem. Um lado sem o outro é metade de um design.

Sétimo, propague a mesma identidade de operação downstream quando a ação de negócio for a mesma. A AWS recomenda explicitamente passar o token de idempotência ao longo da cadeia de processamento.

Oitavo, nunca assuma que o marketing de exatamente uma vez remove a necessidade de efeitos colaterais idempotentes.

Se isso parecer rigoroso, bom. A idempotência é onde a arquitetura otimista encontra a realidade da produção. Você não precisa de complexidade em todos os lugares. Mas, onde efeitos colaterais duplicados poderiam prejudicar dinheiro, estado ou confiança, a idempotência deve ser uma parte de primeira classe do contrato.

Essas mesmas regras se aplicam diretamente a agentes de IA em segundo plano. Agentes de polling que reivindicam tarefas, emitem notificações ou acionam chamadas de ferramentas precisam de chaves de deduplicação e protocolos de reivindicação idempotentes tanto quanto as APIs de pagamento. Para saber como o padrão de reivindicação-deduplicação funciona dentro de assistentes de IA em produção, veja Agentes de Polling em Assistentes de IA: 11 Padrões de Implementação.

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.