Idempotência não é um cabeçalho: o guia do CTO para efeitos Exactly‑Once em 2026

Por Diogo Hudson Dias
Two engineers discussing an idempotency architecture diagram on a whiteboard with API, database, and queue flows.

Se você acha que idempotência é só um cabeçalho HTTP, você já está perdendo dinheiro. Cobranças duplicadas, pedidos em dobro, e-mails fantasma — isso não são casos de borda. É o que acontece em redes reais, com usuários reais que apertam atualizar, SDKs reais que fazem retry automaticamente e, agora, agentes de IA reais que reescrevem e reenviam requisições. O tópico no HN “Idempotency is easy until the second request is different” capturou a dor perfeitamente. Como CTO, você precisa de idempotência que se sustente quando os payloads divergem, as requisições reordenam e os sistemas downstream só são “eventualmente” cooperativos.

Idempotência não é um feature flag. É uma propriedade do sistema.

Idempotência significa: aplicar a mesma operação lógica várias vezes resulta no mesmo efeito, não apenas no mesmo status HTTP. Cabeçalhos ajudam, mas não resolvem condições de corrida, efeitos colaterais parciais ou entrega fora de ordem. A Stripe mantém chaves de idempotência por 24 horas por um motivo; a janela de deduplicação de 5 minutos do AWS SQS FIFO existe por um motivo; as garantias “exactly-once” do Kafka são cuidadosamente delimitadas por um motivo. A rede vai te entregar duplicatas. Seu código também.

De onde realmente vêm as duplicatas

  • Clientes mobile e SDKs fazem retry em timeouts, muitas vezes de 2 a 5 vezes com backoff. Usuários também apertam atualizar. Você recebe duas requisições com conexões TCP diferentes e payloads levemente diferentes (timestamps, nonces).
  • Proxies e balanceadores de carga fazem retry em 502/503/504. Seu app pode ter processado a primeira requisição, mas a resposta foi descartada.
  • Remetentes de webhook (pagamentos, logística) reentregam eventos até verem um 2xx. Você verá entregas fora de ordem e duplicatas — garantido.
  • Agentes de IA amplificam retries. Loops de tool-use vão “ajustar” parâmetros e reenviar mutações quase idênticas.

Se você não tornar idempotência uma preocupação arquitetural de primeira classe, está escolhendo chargebacks e chamados de suporte.

Defina a operação, não o endpoint

Idempotência só faz sentido quando atrelada a uma operação lógica. “Criar o pedido #123 para a conta A com itens X ao preço P” é uma operação. “POST /orders” não é. Modele primeiro o limite da operação:

  • Ator: Quem pode fazer isso? (usuário, conta, serviço)
  • Alvo: Qual recurso está sendo mutado?
  • Intenção: Qual transição de estado esperamos? (carrinho → pedido realizado, pré-autorização → captura)
  • Efeito: Quais efeitos colaterais irreversíveis são disparados? (cobrança, e-mail, decremento de inventário)

Sua chave de idempotência deve mapear para essa unidade lógica, não apenas para uma requisição. Você não consegue deduplicar corretamente se não souber o que “mesmo” significa.

Um framework de decisão para CTOs: sete escolhas que você não pode adiar

1) Escopo das chaves: a quem e a que a chave pertence?

  • Por ator + operação: user_id + operation_type + client_supplied_key. Evita colisões de chaves entre tenants.
  • Por recurso-alvo: Para updates como “definir endereço de entrega”, use resource_id + version (concorrência otimista) em vez de chaves livres.
  • Por estágio do workflow: “authorize” vs “capture” não devem compartilhar o mesmo espaço de chaves.

2) Armazenamento e TTL: por quanto tempo você se lembra?

  • Postgres: Uma única tabela com índice único em (actor, op, key) é durável e simples. Sobrecarga de 1–5 ms no p95 se indexado e com linha pequena (200–800 bytes persistidos: status, checksum, hash da resposta, timestamps).
  • Redis: Ótimo para dedupe na linha de frente com TTL curto (ex., 1–6 horas). Combine com Postgres para durabilidade dos resultados.
  • DynamoDB/Cosmos: Encaixe natural para workloads globais com writes condicionais. Seja explícito sobre TTL e capacidade de escrita.
  • Política de TTL: Ajuste ao risco do negócio. A Stripe documenta 24 horas; a dedupe do SQS FIFO é 5 minutos; webhooks frequentemente reentregam por dias. Para pedidos e pagamentos, 24–72 horas é sensato; para ações efêmeras, 15–60 minutos é suficiente.

3) Repetição de resultado: você retorna os mesmos bytes ou apenas o mesmo efeito?

  • Armazene um envelope de resposta: status HTTP, headers relevantes para clientes (cache-control, ETag) e um corpo JSON estável (ou um hash + ponteiro).
  • Determinismo: Remova campos não determinísticos (timestamps, IDs aleatórios) ou mantenha-os estáveis entre replays para evitar confusão do cliente.

4) Política de conflito de payload: o problema do “a segunda requisição é diferente”

  • Hash canônico: Persista um hash do payload canonizado (chaves ordenadas, strings aparadas, decimais normalizados).
  • Em caso de divergência: Retorne 409 Conflict com o corpo de resposta original e explique o payload canônico que “venceu”. Nunca aceite silenciosamente uma mutação diferente sob a mesma chave.
  • Upgrades: Se você oferecer “idempotency + patch”, exija uma nova chave, mas permita vincular via parent_operation_id.

5) Controle de concorrência: pare corridas, não só duplicatas

  • Primeira escrita atômica: Use um INSERT … ON CONFLICT DO NOTHING (ou conditional put) para reivindicar a chave antes de executar o efeito. Se você perder a corrida, consulte o registro do vencedor e repita o resultado dele.
  • Advisory locks: Para chaves quentes (ex., o mesmo carrinho), um advisory lock de curta duração por recurso evita efeito manada sem locks globais grosseiros.
  • Timeouts: Se o dono de uma chave reivindicada cair, libere após um período de graça e marque o registro como “indeterminado”, forçando um caminho de reconciliação.

6) Isolamento de efeitos colaterais: o padrão outbox/inbox

  • Outbox: Grave a mudança de estado do domínio e um “evento a publicar” na mesma transação. Um relé publica do outbox para Kafka/SNS e marca como enviado. Evita o split “atualizou o BD mas falhou ao publicar”.
  • Inbox: Para consumidores (especialmente webhooks), faça dedupe em (source, event_id) antes de aplicar estado. Persista o desfecho. Retorne 2xx somente após um commit durável.
  • E-mails/SMS idempotentes: Armazene um message_id e suprima duplicatas dentro de uma janela. Fornecedores downstream também fazem retry.

7) Limites de “exactly-once”

  • Filas: O produtor idempotente do Kafka e transações dão processamento exactly-once dentro do Kafka. No momento em que você chama uma API externa, você volta ao at-least-once. Projete com essa honestidade.
  • Datastores: Mesmo dentro de um único banco, “exactly-once” depende de suas restrições únicas e do modelo de transação. Teste a matriz de falhas (timeout após commit, crash do processo, partição de rede).

Uma implementação de referência pragmática

Postgres em primeiro lugar: sem glamour, correto, rápido o suficiente

  • Tabela: idempotency_keys(actor_id, op, key, payload_hash, status, response_blob, created_at, updated_at, claimed_by, expires_at). Índice único em (actor_id, op, key).
  • Fluxo: O cliente envia Idempotency-Key. O servidor canoniza o payload e calcula payload_hash. INSERT para reivindicar. Se der sucesso, faça o trabalho; armazene response_blob; defina status=completed. Se houver conflito, SELECT na linha existente; se o payload_hash bater e status=completed, retorne a resposta armazenada; se estiver em progresso, faça polling com backoff; se o hash divergir, retorne 409 com a resposta do vencedor.
  • Números: Com uma resposta JSONB abaixo de 8 KB e indexação adequada, a latência extra no p95 de 1–3 ms no Postgres 14+ em hardware intermediário é típica. O custo de storage é de algumas centenas de bytes por requisição mais o tamanho da resposta; com TTL de 72 horas você geralmente fica em poucos GB para a maioria das APIs SaaS.

Aceleração com Redis: cache de frente, Postgres como verdade

  • SETNX + TTL curto para controlar o hot path; se o set tiver sucesso, prossiga e grave o registro durável no Postgres; se não, busque no Postgres.
  • Script Lua para comparar atomicamente o hash do payload e definir marcadores de claim para evitar corridas breves.
  • Trade-off: A complexidade aumenta, mas você economiza 1–2 ms no p95 em endpoints muito quentes. Não confie só no Redis para durabilidade crítica de negócio.

Webhooks: inbox ou não aconteceu

  • Tabela de deliveries indexada por (provider, event_id). Restrição única + status + ponteiro para efeito de negócio.
  • Retorne 200 apenas depois que a transição de estado do domínio for confirmada (commit). Se você precisar de trabalho longo, reconheça rápido e encaminhe para uma fila.
  • Reordenação: Use versão de evento (ex., seq) e processe com segurança mesmo se N+1 chegar antes de N. Isso significa que seus handlers também devem ser idempotentes.

SQS/Kafka: seja honesto sobre as garantias

  • SQS FIFO oferece entrega ordenada e dedup de 5 minutos. Ainda assim, projete consumidores para lidar com duplicatas e replays fora dessa janela.
  • Kafka: Use produtores idempotentes e transações para EOS intra-Kafka. Ao fazer bridge para sistemas externos, volte ao at-least-once com outbox e inbox no lado do consumidor.

Tratando “mesma chave, payload diferente” sem drama

É aqui que a maioria das equipes quebra. Decida e documente a política:

  • Conflito rígido: Se o hash diferir, retorne 409 com a resposta original e o payload canônico que reivindicou a chave. Registre um evento de auditoria estruturado.
  • Upgrade suave: Permita mudanças menores (ex., adicionar um campo de observação). Implemente um hash de esquema de payload que ignore campos não semânticos. Requer disciplina — não abuse.
  • Nova intenção, nova chave: Para qualquer mudança material (valor, itens, endereço), exija uma chave nova com um parent_operation_id opcional. Ensine seus SDKs a fazer isso por padrão.

Observabilidade: você não gerencia o que não vê

  • Logue o envelope: actor_id, op, key, payload_hash, status, winner_node, duration_ms, replayed=true/false.
  • Métricas: duplicates_blocked_per_route, in_progress_timeouts, conflict_rate, replay_rate, p95_claim_latency. Se conflict_rate disparar após um deploy, você quebrou a canonização do payload.
  • Tracing: Propage operation_id pelos seus spans. Marque efeitos downstream (email_id, charge_id) para ver fan-out e retries.

Agentes de IA tornam idempotência inegociável

Agentes não se entediam; eles fazem retry. Eles também “ajudam” mutando payloads (“arredondei o valor para duas casas decimais”). Se você expõe ferramentas internas a agentes, imponha um envelope de mutação:

  • operation_id: Estável por intenção, gerado no primeiro plano.
  • intent_hash: Hash de campos de negócio canônicos (não de formatação).
  • capabilities_version: Assim você pode rejeitar planos antigos que não batem com os contratos atuais.
  • somente chaves no servidor: Não confie em chaves fornecidas por agentes via prompts ou tools. Emita por sessão, com escopo.

Segurança e controle de abuso

  • Tamanho e formato da chave: 16–32 bytes de aleatoriedade (base64/hex). Rejeite chaves absurdas para prevenir abuso de memória.
  • TTL + cotas: Aplique limites por tenant para novas chaves por minuto. Uma enxurrada de chaves únicas é um vetor de DoS.
  • Criptografia em repouso: Blobs de resposta podem conter PII. Trate o repositório de idempotência como dado de aplicação, não como logs.

Plano de rollout: 30/60/90 dias

Dia 0–30: escolha o escopo, construa os trilhos sem glamour

  • Identifique os 5 endpoints de mutação de maior receita e risco (pagamentos, pedidos, inventário, créditos).
  • Implemente o padrão “Postgres em primeiro lugar” com restrições únicas e replay de resposta para essas rotas.
  • Instrumente métricas e logs desde o dia um. Bloqueie payloads conflitantes com 409.

Dia 31–60: integre webhooks e efeitos colaterais

  • Adicione dedupe via inbox para webhooks dos seus três principais provedores. Persista os desfechos.
  • Leve e-mails/SMS para envios idempotentes chaveados por message_id. Suprima duplicatas em 24 horas.
  • Introduza outbox para eventos de domínio que fazem fan-out para Kafka/SNS. Valide o caminho de recuperação após crash/timeout.

Dia 61–90: endureça e escale

  • Adicione cache de frente com Redis se o p95 piorar após a adoção. Mantenha o Postgres como fonte da verdade.
  • Faça um drill de caos: injete timeouts após commit, entregas duplicadas e eventos reordenados. Prove que não há cobranças em dobro nem pedidos fantasma.
  • Ensine seus SDKs e integradores parceiros a enviar chaves de forma consistente. Documente publicamente a política de conflitos.

Trade-offs que você deve aceitar de início

  • Overhead de storage: Você vai armazenar um registro por mutação lógica por 24–72 horas. É um seguro barato comparado a mesmo alguns chargebacks.
  • Imposto de latência: Espere 1–3 ms no p95 no caminho de claim/leitura em um banco relacional bem ajustado. Se isso for demais, você está otimizando a camada errada.
  • Rigor custa suporte: Retornar 409 para payloads conflitantes gera chamados. Tudo bem. Melhor um toque no suporte do que corrupção silenciosa de dados.
  • Nem tudo precisa de chave: Leituras puras e PUTs realmente idempotentes (definir valor exato) não precisam dessa maquinaria. Seja seletivo.

Como é o ideal

  • Cada rota de mutação documenta seu limite de operação e a política de conflitos.
  • Cada resposta inclui um operation_id para que clientes possam correlacionar replays.
  • Duplicatas são observáveis: você consegue mostrar um dashboard de duplicatas bloqueadas por rota e por tenant.
  • Webhooks são seguros para replay indefinidamente: a tabela de inbox impõe dedupe, os handlers são idempotentes.
  • Efeitos colaterais saem de outboxes, e você pode matar um worker no meio do voo sem enviar em duplicidade.

Idempotência que sobrevive a retries, reordenações e ao “a segunda requisição é diferente” não é over-engineering. É o nível mínimo para sistemas que movem dinheiro e tomam pedidos em 2026. Trate isso como requisito de negócio, não preferência de desenvolvedor, e suas páginas — e seu financeiro — ficarão quietos.

Pontos-chave

  • Idempotência é uma propriedade do sistema ancorada a uma operação lógica, não um cabeçalho de requisição.
  • Decida antecipadamente escopo, armazenamento, TTL, estratégia de replay, política de conflito, controle de concorrência e isolamento de efeitos colaterais.
  • Use um storage durável (Postgres/DynamoDB) com restrição única; Redis pode acelerar, mas não deve ser sua fonte da verdade.
  • Trate “mesma chave, payload diferente” com hashes canônicos e 409 Conflict; nunca aceite deriva silenciosa.
  • Adote os padrões outbox/inbox para tornar efeitos externos (eventos, e-mails, webhooks) seguramente idempotentes.
  • Meça duplicatas, conflitos e latência p95 de claim; teste caos de retries e reordenações antes que os clientes o façam.
  • Agentes de IA aumentam retries e deriva de payload; imponha um envelope de mutação com IDs de operação estáveis.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation