Idempotência em Sistemas Distribuídos que Realmente Funciona
Evitar efeitos colaterais duplicados
A idempotência em sistemas distribuídos é a propriedade que te salva depois que a rede falha, a fila retransmite, o cliente entra em pânico e o operador clica em “replay”. Em sistemas de produção, a entrega duplicada é normal. Os 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 de 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 timeout entre o commit e a resposta. Se um trabalhador atualiza o estoque e trava antes de confirmar 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. A deduplicação de filas, os verbos HTTP e as 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 por 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 os sistemas distribuídos retransmitem, reordenam e replay.
Um cliente pode enviar uma solicitação de criação, o servidor pode commitá-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. A 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 (ack), causando a reentrega da mesma mensagem pelo broker.
Os 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 reentregues automaticamente e cada entrega carrega um GUID único X-GitHub-Delivery que você deve usar ao se proteger contra replay. Para uma visão arquitetural prática de endpoints de chat como fronteiras de interação, veja 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 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 requerem coordenação com offsets e saídas. A entrega “exatamente uma vez” do Google Cloud Pub/Sub é limitada a assinaturas de puxar (pull), dentro de uma região de nuvem e ainda requer que os clientes rastreiem o progresso do processamento até que o reconhecimento tenha sucesso.
Meu resumo de opinião é simples. Assuma que o transporte vai retransmitir. Assuma que os operadores farão replay. Assuma que os 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 contrato de API que eu realmente confio
Como as chaves de idempotência previnem solicitações de API duplicadas
O único contrato de API 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 os parâmetros posteriores com a solicitação original e retorna o mesmo resultado para retransmissões. O PayPal usa PayPal-Request-Id nas APIs POST suportadas e retorna o status mais recente para a 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 tenant e nome da operação.
- O servidor armazena um hash da solicitação para que a mesma chave não possa ser reutilizada para um payload diferente.
- O servidor registra o estado, como
pendente,concluídooufalhou. - Retransmissões com a mesma chave retornam o resultado armazenado ou um ponteiro estável para ele.
- Retransmissões com a mesma chave e um payload 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 Datatracker do IETF 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 facto, 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 de 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 dashboards parecerão organizados 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á ao PUT semânticas idempotentes. Mas se o manipulador do seu 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. Ela 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 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. A deduplicação FIFO do Amazon SQS funciona 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ê mantiver 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 pela máxima destas janelas:
- horizonte de retransmissão do cliente
- horizonte de redrive da fila
- horizonte de replay do 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 duas anti-padrões com as quais concordo plenamente. 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 payloads de solicitação inteiros como o 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 de resposta mínimo necessário para replay com segurança. Se você tiver que reproduzir o primeiro byte da resposta, armazene o corpo de 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 vencer uma corrida exatamente uma vez.
O PostgreSQL oferece dois primitivos críticos aqui. Restrições de unicidade 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-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 ser assim:
iniciar transação
tentar inserir (tenant_id, operation, idempotency_key, request_hash, state='pendente')
em conflito não fazer nada
carregar linha para (tenant_id, operation, idempotency_key) para atualização
se row.request_hash != incoming_request_hash
falhar com erro de conflito ou validação
se row.state = 'concluído'
retornar resposta armazenada
se row.state = 'pendente' e a linha foi criada por outra solicitação ativa
esperar brevemente, ou falhar rapidamente com uma resposta retransmissível
executar mutação de negócios local
armazenar resultado estável na linha de idempotência
definir state = 'concluído'
commit
retornar resultado
A parte importante não é a sintaxe. A parte importante é a atomicidade. Registrar a chave e executar 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 com suporte SQL.
Não faça uma sequência ingênua de “verificar-então-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 de unicidade não é opcional. É o mecanismo que transforma sua arquitetura de 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 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 os IDs de mensagens processadas na mesma transação de banco de dados que a atualização de negócios. Chris Richardson descreve a abordagem da tabela PROCESSED_MESSAGES diretamente, usando uma chave primária em assinante e ID de mensagem para que duplicados falhem limpa e possam ser ignorados.
Muitas equipes chamam esse armazenamento explícito de 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á lidou com a mensagem antes que uma retransmissão possa fazer nada com segurança.
Uma forma mínima parece assim:
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:
iniciar transação
inserir em processed_messages (subscriber_id, message_id)
valores (?, ?)
em conflito não fazer nada
se nenhuma linha inserida
rollback
ack e ignorar duplicado
aplicar mutação de negócios
commit
ack mensagem
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 envios duplicados apenas dentro de sua janela de deduplicação de 5 minutos. O “exatamente uma vez” do Pub/Sub ainda espera que o assinante rastreie 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 de 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 de outbox transacional é importante. Chris Richardson descreve a ideia básica como escrever o evento em uma tabela de outbox na mesma transação que a atualização de negócios, e então publicá-lo assincronamente. O Debezium diz que o padrão de 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 de 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 de outbox em uma única transação local.
- Permita que o CDC ou um despachante de outbox publique o evento.
- Torne os consumidores downstream também idempotentes.
O 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 transação distribuída mágica única 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 autenticidade e fornece X-GitHub-Delivery como o identificador de entrega único. Ele também nota 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 o manipulador do seu webhook escrever diretamente nas 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 workflow ainda precisam de idempotência
Sagas e motores de workflow 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 timeouts. Seus documentos até destacam o caso de borda onde um trabalhador completa um efeito colateral externo com sucesso, mas trava antes de relatar a conclusão, o que causa a execução da Atividade novamente. O Temporal também sugere usar uma combinação de Workflow Run ID e Activity ID 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 cobre as compensações de workflow mais amplas.
Este é exatamente o modelo mental correto. Um motor de workflow pode preservar o histórico de execução e coordenar retransmissões. Ele não pode retroativamente descarregar um cartão ou desmandar um e-mail a menos que sua aplicação lhe dê 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 fica surpresa quando as retransmissões acontecem. Isso não é suficiente.
Você deve ter testes automatizados para pelo menos estes casos:
- o servidor commita 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 um payload diferente
- um consumidor commita seu trabalho de banco de dados e trava antes do ack
- um webhook é replayado com o mesmo ID de entrega
- um despachante de outbox publica o mesmo evento mais de uma vez
- uma Atividade de workflow completa a chamada externa e trava antes que a conclusão seja relatada
- um registro de idempotência expira e uma retransmissão atrasada genuína 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 replayada é semanticamente equivalente ao primeiro resultado. A AWS discute retransmissões que chegam atrasadas 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 de opinião que salvam sistemas reais
Aqui estão as regras que eu exigiria em uma revisão de arquitetura.
Primeiro, as chaves de idempotência pertencem à intenção de negócios, não às tentativas de transporte.
Segundo, delimite cada chave por tenant 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 payload-diferente. Stripe e AWS fazem isso por boas razões.
Quinto, mantenha chaves pelo horizonte de replay 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 soar estrito, bom. A idempotência é onde a arquitetura otimista encontra a realidade de 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.