Idempotência em Sistemas Distribuídos Que Realmente Funciona
Evitar efeitos colaterais duplicados
Idempotência em sistemas distribuídos é a propriedade que salva você depois que a rede falha, a fila retransmite, o cliente entra em pânico e o operador clica em reproduzir. Em sistemas de produção, a entrega duplicada é normal. Efeitos colaterais duplicados são o bug.
O HTTP define um método idempotente como aquele em que múltiplas solicitações idênticas têm o mesmo efeito pretendido no servidor do que uma única solicitação. É por isso que PUT, DELETE e métodos seguros são idempotentes na semântica do protocolo e podem ser retransmitidos automaticamente após uma falha de comunicação.

Essa definição é útil, mas não é suficiente. Em arquiteturas reais, a idempotência não é uma resposta de trivialidade do HTTP. É uma garantia de negócios. Se um cliente clica em “pagar” uma vez, você não tem o direito de cobrar duas vezes porque houve um tempo limite entre o compromisso e a resposta. Se um trabalhador atualiza o inventário e falha antes de reconhecer a mensagem, você não tem o direito de decrementar o estoque duas vezes porque o broker reentregou. Esse é o padrão.
O erro que vejo repetidamente é tratar a idempotência como um recurso de transporte em vez de uma propriedade do sistema. Deduplicação de fila, verbos HTTP e retransmissões do cliente ajudam, mas nenhum deles salva um design que permite que a mesma intenção de negócios crie um segundo efeito colateral. Se você deseja uma visão mais ampla de como essas decisões de integração se encaixam nas fronteiras de serviço e nas compensações de persistência, comece com Arquitetura de Aplicação em Produção: Padrões de Integração, Design de Código e Acesso a Dados.
De onde vêm os duplicados em produção
Os duplicados não aparecem porque as equipes são descuidadas. Eles aparecem porque sistemas distribuídos retransmitem, reordenam e reproduzem.
Um cliente pode enviar uma solicitação de criação, o servidor pode comprometê-la e a resposta ainda pode desaparecer na rede. É 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. Entrega pelo menos uma vez 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 do reconhecimento, fazendo com que o broker entregue a mesma mensagem novamente.
Webhooks não são diferentes. O GitHub afirma que as entregas de webhook podem chegar fora de ordem, as entregas com falha não são reenviadas automaticamente e cada entrega carrega um GUID único X-GitHub-Delivery que você deve usar ao se proteger contra reprodução. Para uma visão arquitetural prática de endpoints de chat como fronteiras de interação, consulte Plataformas de Chat como Interfaces de Sistema em Sistemas Modernos.
Mesmo sistemas que anunciam garantias mais fortes ainda deixam trabalho para você fazer. O Kafka pode prevenir entradas duplicadas em 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 a própria documentação de design do Kafka é clara de que sistemas externos ainda requerem coordenação com offsets e saídas. A entrega exatamente uma vez do Google Cloud Pub/Sub é limitada a assinaturas de pull, dentro de uma região de nuvem e ainda exige que os clientes acompanhem o progresso do processamento até que o reconhecimento seja bem-sucedido.
Meu resumo opinativo é simples. Assuma que o transporte irá retransmitir. Assuma que os operadores irão reproduzir. Assuma que webhooks chegarão atrasados. Projete o caminho de escrita para que uma intenção repetida não possa criar um segundo efeito de negócios. O design de erros está intimamente relacionado: como os erros são embrulhados, traduzidos e classificados como retransmissíveis versus não retransmissíveis faz parte da mesma disciplina de fronteira — Arquitetura de Tratamento de Erros em Go: Fronteiras e Padrões abrange a classificação de erros retransmissíveis, tradução de fronteira e os padrões sentinela que permitem que a lógica de retransmissão tome decisões sensatas.
O contrato de API em que eu realmente confio
Como as chaves de idempotência previnem solicitações de API duplicadas
O único contrato de API em que confio para operações de mutação é 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 de mutação. A Stripe armazena o primeiro código de status e corpo da resposta para uma chave, compara parâmetros posteriores com a solicitação original e retorna o mesmo resultado para retransmissões. O PayPal usa PayPal-Request-Id em APIs POST suportadas e retorna o status mais recente da solicitação anterior com o mesmo cabeçalho.
Isso leva a um contrato prático:
- O cliente gera uma chave de idempotência para uma operação de negócios.
- O servidor delimita essa chave por inquilino e nome da operação.
- O servidor armazena um hash de solicitação para que a mesma chave não possa ser reutilizada para uma carga útil diferente.
- O servidor registra o estado, como
pendente,concluídooufalhou. - Retransmissões com a mesma chave retornam ou o resultado armazenado ou um ponteiro estável para ele.
- Retransmissões com a mesma chave e uma carga útil diferente falham de forma explícita.
Existe um rascunho de cabeçalho Idempotency-Key do IETF, mas até 09/05/2026 ele 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 retransmissão. Se o usuário significa “criar pedido 123 uma vez”, cada retransmissão para esse mesmo comando deve reutilizar a mesma chave. Se o usuário significa “realizar 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ê misturar isso, seus painéis parecerão limpos enquanto seu dinheiro se move duas vezes.
Por que o PUT não é suficiente
Não, o HTTP PUT não é suficiente para tornar uma operação idempotente.
Sim, o RFC 9110 dá semânticas idempotentes ao PUT. Mas se seu manipulador PUT emite um novo evento downstream, envia um e-mail em cada retransmissão ou cobra um provedor externo novamente, então sua implementação violou o contrato de negócios, mesmo que o nome da sua rota pareça respeitável.
A escolha do verbo ajuda os clientes a entenderem a intenção. Ele não implementa a intenção para você.
Use PUT quando o modelo de recurso realmente se encaixar em uma operação de substituição total ou estilo upsert. Use POST quando estiver criando comandos ou ações. Mas para qualquer mutação que possa ser retransmitida através de fronteiras de rede, documente um contrato de idempotência explícito. Se suas ações de mutação 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 Loops de Controle. Efeitos colaterais ocultos são onde a arquitetura vai morrer.
Por quanto tempo uma chave de idempotência deve ser armazenada
Mais tempo do que sua equipe de transporte deseja.
A 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 dá exemplos que podem durar até 45 dias. O Amazon SQS FIFO deduplica apenas dentro de uma janela de 5 minutos. O GitHub mantém entregas recentes por 3 dias para reentrega manual. Esses números são amplamente diferentes porque o período de retenção correto é uma decisão de negócios, não um padrão de protocolo.
Se você manter chaves apenas por cinco minutos porque sua fila faz isso, você não está projetando idempotência. Você está copiando uma limitação de transporte para sua camada de negócios.
Mantenha registros de idempotência pelo menos pelo máximo dessas janelas:
- horizonte de retransmissão do cliente
- horizonte de redirecionamento da fila
- horizonte de reprodução do webhook
- horizonte de reprodução do operador
- horizonte de liquidação ou compensação para operações de movimentação de 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 totalmente. Não use timestamps como chave, porque o desvio de relógio e colisões os tornam não confiáveis. Não armazene cegamente cargas úteis de solicitação inteiras 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 reproduzir com segurança. Se você precisar reproduzir o primeiro byte de resposta por byte, armazene o corpo da resposta canônico como a 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 ganhar uma corrida exatamente uma vez.
O PostgreSQL oferece dois primitivos críticos aqui. Restrições únicas impõem 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 de inserção ou atualização atômico sob concorrência.
Isso significa que sua camada de idempotência deve geralmente começar com uma tabela assim:
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 isso:
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 deve ter sucesso ou falhar juntos. A AWS diz isso explicitamente para idempotência de API, e a mesma regra se aplica em serviços com suporte SQL.
Não faça uma sequência ingênua de verificar-e-agir como “selecionar chave; se ausento então inserir pedido”. Sob concorrência, duas solicitações podem passar na verificação e ambas criar o efeito colateral. Uma restrição única não é opcional. É o mecanismo que transforma sua arquitetura de folclorismo 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 estiver protegida pela mesma fronteira transacional da mutação, você não tem idempotência. Você tem esperança.
Mensagens, eventos e webhooks precisam de sua própria fronteira
Como os consumidores lidam com eventos e mensagens duplicadas
Para consumidores de mensagens, o padrão clássico ainda é o correto. Registre IDs de mensagens processadas na mesma transação de banco de dados que a atualização de negócios. Chris Richardson descreve diretamente a abordagem da tabela PROCESSED_MESSAGES, usando uma chave primária no assinante e ID da mensagem para que duplicatas falhem limpa e possam ser ignoradas.
Muitas equipes chamam esse armazenamento explícito de processed_messages de tabela de caixa de entrada. A etiqueta importa menos do que a regra. O receptor deve persistir prova de que já lidou com a mensagem antes que uma retransmissão possa fazer nada com segurança.
Uma forma mínima parece com isso:
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 confiar em termos de marketing de broker. O suporte exatamente uma vez do Kafka é excelente quando você permanece dentro do próprio modelo transacional do Kafka, mas a documentação do Kafka ainda alerta que destinos externos precisam de cooperação. O SQS FIFO reduz envios duplicados apenas dentro de sua janela de deduplicação de 5 minutos. O Pub/Sub exatamente uma vez ainda espera que o assinante acompanhe o progresso e evite trabalho duplicado quando os reconhecimentos falham.
Exatamente uma vez geralmente é uma otimização local. Efeitos colaterais idempotentes são a garantia do sistema.
Combine deduplicação com o padrão outbox
Se 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 outbox transacional 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ócios 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 outbox deduplica mensagens recebidas e evita registros zumbis e mensagens fantasmas.
Esta é a arquitetura que recomendo para serviços que possuem dados e publicam eventos de integração:
- Valide e persista o comando sob uma chave de idempotência.
- Escreva o estado de negócios e o evento outbox em uma única transação local.
- Permita que o CDC ou um despachante outbox publique o evento.
- Torne os consumidores downstream também idempotentes.
Outbox não remove a necessidade de consumidores idempotentes. Ele remove a necessidade de fingir que um commit de banco de dados e uma publicação de broker podem ser uma única transação distribuída mágica quando geralmente não podem.
Webhooks são apenas mensagens com melhor branding
Trate webhooks de entrada exatamente como mensagens de uma borda de rede não confiável.
O GitHub documenta que as entregas podem chegar fora de ordem, recomenda o uso de X-Hub-Signature-256 para verificar a autenticidade e fornece X-GitHub-Delivery como o identificador único de entrega. 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 chave de deduplicação
- persista o recebimento antes dos efeitos colaterais
- torne os manipuladores conscientes da ordem em vez de assumir a ordem de chegada
- enfileire o trabalho pesado e retorne rápido
Se seu manipulador de webhook escrever diretamente em tabelas de negócios antes de registrar o recebimento, ele não está pronto para produção. É apenas mais rápido em 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.
O Temporal recomenda escrever Atividades para serem idempotentes porque as Atividades podem ser retransmitidas após falhas ou tempos limite. Sua documentação até destaca o caso de borda onde um trabalhador completa um efeito colateral externo com sucesso, mas falha antes de relatar a conclusão, o que faz com que a Atividade seja executada novamente. O Temporal também sugere usar uma combinação do ID de Execução do Fluxo de Trabalho e do ID da Atividade como uma chave de idempotência estável ao chamar serviços downstream. Se você estiver aplicando isso em orquestração de serviços, Microsserviços Go para Orquestração de IA/ML abrange as compensações de fluxo de trabalho mais amplas.
Esse é exatamente o modelo mental certo. Um motor de fluxo de trabalho pode preservar o histórico de execução e coordenar retransmissões. Ele não pode descarregar um cartão ou desenviar um e-mail retroativamente, a menos que seu aplicativo forneça etapas idempotentes e compensações idempotentes.
O mesmo se aplica a sagas. A própria orientação de saga do Temporal descreve ações compensatórias 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 cada 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 idempotência antes da produção
A maioria das equipes testa caminhos felizes e então age surpreso quando retransmissões acontecem. Isso não é suficiente. Para equipes Go, Testando Código Go Concorrente com testing/synctest cobre como escrever testes rápidos e determinísticos para loops de retransmissão e comportamento de limite de contexto sem dormir através de atrasos artificiais.
Você deve ter testes automatizados para pelo menos estes casos:
- o servidor compromete a mutação, mas a resposta nunca atinge o cliente
- duas solicitações idênticas competem com a mesma chave de idempotência
- a mesma chave é reutilizada com uma carga útil diferente
- um consumidor compromete seu trabalho no banco de dados e falha antes do ack
- um webhook é reproduzido com o mesmo ID de entrega
- um despachante outbox publica o mesmo evento mais de uma vez
- uma Atividade de fluxo de trabalho completa a chamada externa e falha antes que a conclusão seja relatada
- um registro de idempotência expira e uma retransmissão verdadeira atrasada chega
A AWS recomenda explicitamente suítes de teste abrangentes que incluam solicitações bem-sucedidas, solicitações com falha e solicitações duplicadas. Esse conselho é pedestre e absolutamente correto.
Eu adicionaria mais um exercício de falha. Verifique que a resposta reproduzida é semanticamente equivalente ao primeiro resultado. A AWS discute retransmissões 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ócios, não a tentativas de transporte.
Segundo, delimite cada chave por inquilino e operação. Espaços de chaves 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 retransmissões de mesma-chave e diferente-carga. Stripe e AWS fazem isso por boas razões.
Quinto, mantenha chaves pelo horizonte de reprodução completo do processo de negócios, não pela janela de fila mais curta.
Sexto, combine 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ócios 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 soa 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 quer que efeitos colaterais duplicados prejudiquem 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 ferramenta precisam de chaves de deduplicação e protocolos de reivindicação idempotentes tanto quanto APIs de pagamento. Para saber como o padrão de reivindicação e deduplicação funciona dentro de assistentes de IA de produção, consulte Agentes de Polling em Assistentes de IA: 11 Padrões de Implementação.