Idempotência em Sistemas Distribuídos que Realmente Funciona

Evitar efeitos colaterais duplicados

Conteúdo da página

A idempotência em sistemas distribuídos é a propriedade que te salva quando a rede falha, a fila reenvia, o cliente entra em pânico e o operador executa uma reprodução. 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 reenviados automaticamente após uma falha de comunicação.

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

Essa definição é útil, mas não é suficiente. Em arquiteturas reais, a idempotência não é uma resposta de trivialidade sobre HTTP. É uma garantia de negócio. 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 commit e a resposta. Se um trabalhador atualiza o inventário e trava antes de confirmar a mensagem, você não tem o direito de decrementar o estoque duas vezes porque o broker reentregou a mensagem. 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, verbos HTTP e reenvios do cliente ajudam, mas nenhum deles salva um design que permite que a mesma intenção de negócio 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 reenviam, reordenam e reproduzem.

Um cliente pode enviar uma solicitação de criação, o servidor pode confirmá-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, causando o reenvio 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 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 prática de arquitetura 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 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 pull, dentro de uma região de nuvem e ainda requer que os clientes acompanhem o progresso do processamento até que o reconhecimento seja bem-sucedido.

Meu resumo opinado é simples. Assuma que o transporte vai reenviar. Assuma que os operadores vão reproduzir. Assuma que os webhooks vão chegar atrasados. Projete o caminho de escrita para que uma intenção repetida não possa criar um segundo efeito de negócio.

O contrato de API em que eu realmente confio

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

O único contrato de API em que confio para operações mutantes é a intenção fornecida pelo chamador mais 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 reenvios. O PayPal usa PayPal-Request-Id em 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:

  1. O cliente gera uma chave de idempotência para uma operação de negócio.
  2. O servidor delimita essa chave por locatário e nome da operação.
  3. O servidor armazena um hash de solicitação para que a mesma chave não possa ser reutilizada para uma carga útil diferente.
  4. O servidor registra o estado, como pendente, concluído ou falhou.
  5. Reenvios com a mesma chave retornam ou o resultado armazenado ou um ponteiro estável para ele.
  6. Reenvios com a mesma chave e uma carga útil diferente falham de forma clara.

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 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 reenvio. Se o usuário significa “criar pedido 123 uma vez”, cada reenvio 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 esses dois, seus painéis 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 seu manipulador de PUT emitiu um novo evento downstream, enviou um e-mail em cada reenvio ou cobrou um provedor externo novamente, então sua implementação violou o contrato de negócio, mesmo que o nome da sua rota pareça respeitável.

A escolha do verbo ajuda os clientes a entenderem a intenção. Isso 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 reenviada através de limites 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 eliminadas 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 selvagemente diferentes porque o período de retenção correto é uma decisão de negócio, 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ócio.

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

  • horizonte de reenvio 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 que movimentam dinheiro

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

A AWS também destaca duas antipadrõ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 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 mínimo de resposta necessário para reproduzir com segurança. Se você precisar 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 se torna real quando a camada de persistência pode vencer uma corrida exatamente uma vez.

O PostgreSQL oferece duas primitivas críticas 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 atômico de inserção-ou-atualização 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:

iniciar transação

tentar inserir (tenant_id, operation, idempotency_key, request_hash, state='pending')
em conflito fazer nada

carregar linha para (tenant_id, operation, idempotency_key) para atualização

se row.request_hash != incoming_request_hash
    falhar com conflito ou erro de validação

se row.state = 'completed'
    retornar resposta armazenada

se row.state = 'pending' e row foi criada por outra solicitação ativa
    ou esperar brevemente, ou falhar rapidamente com uma resposta reenviável

executar mutação de negócio local

armazenar resultado estável na linha de idempotência
definir state = 'completed'

confirmar
retornar resultado

A parte importante não é a sintaxe. A parte importante é a atomicidade. Registrar a chave e executar 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 ausente 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 folclore otimista em algo que você pode provar sob carga.

Esta é a regra que uso nas revisões. Se a decisão de deduplicação não estiver protegida pelo mesmo limite 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ócio. Chris Richardson descreve a abordagem da tabela PROCESSED_MESSAGES diretamente, 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 processed_messages de tabela de entrada. O rótulo importa menos do que a regra. O receptor deve persistir a prova de que já manipulou a mensagem antes que um reenvio 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:

iniciar transação

inserir em processed_messages (subscriber_id, message_id)
valores (?, ?)
em conflito fazer nada

se nenhuma linha inserida
    rollback
    confirmar e ignorar duplicata

aplicar mutação de negócio

confirmar
confirmar 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 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.

Pare a 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 enviar o evento após a confirmação da transação local.

É por isso que o padrão de outbox transacional importa. 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ócio, 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 mais longe e mostra como o processamento de outbox deduplica mensagens de entrada e evita registros zumbis e mensagens fantasma.

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 de outbox em uma única transação local.
  3. Deixe o CDC ou um despachante de outbox publicar o evento.
  4. 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 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 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ócio 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.

A Temporal recomenda escrever Atividades para serem idempotentes porque as Atividades podem ser reenviadas após falhas ou tempos limite. 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. A Temporal também sugere usar uma combinação de ID de Execução de Fluxo de Trabalho e ID de Atividade 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 de fluxo de trabalho mais amplas.

Este é exatamente o modelo mental correto. Um motor de fluxo de trabalho pode preservar o histórico de execução e coordenar reenvios. Ele não pode desadicionar um cartão ou desenviar um e-mail retroativamente, a menos que seu aplicativo lhe dê etapas idempotentes e compensações idempotentes.

O mesmo se aplica a sagas. A própria orientação de saga da 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 interage com o mundo exterior 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 fica surpresa quando reenvios acontecem. Isso não é suficiente.

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

  • o servidor confirma 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 confirma seu trabalho de banco de dados e trava antes de confirmar
  • um webhook é reproduzido com o mesmo ID de entrega
  • um despachante de outbox publica o mesmo evento mais de uma vez
  • uma Atividade de fluxo de trabalho completa a chamada externa e trava antes que a conclusão seja relatada
  • um registro de idempotência expira e um reenvio atrasado genuíno chega

A AWS recomenda explicitamente suites 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 teste de falha. Verifique que a resposta reproduzida é semanticamente equivalente ao primeiro resultado. A AWS discute reenvios que chegam atrasados 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 opinadas 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 a tentativas de transporte.

Segundo, delimite cada chave por locatário 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 reenvios de mesma-chave e diferente-carga-útil. A Stripe e a AWS fazem isso por um bom motivo.

Quinto, mantenha chaves pelo horizonte completo de reprodução do processo de negócio, não 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 soa 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.

Assinar

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