O botão do elevador
Apertar o botão cinco vezes não chama cinco elevadores: a primeira aperta, as outras encontram o pedido já registrado e não mudam nada. O painel 'lembra' que aquela intenção já existe. A chave de idempotência é isso — um jeito de o servidor reconhecer 'esse pedido eu já vi' e não executar de novo.
O problema: o timeout não diz o que aconteceu
Seu app chama a API de pagamento e a conexão cai depois de 30 segundos. O dinheiro saiu? Você não sabe: o timeout pode ter acontecido antes de o servidor processar ou depois — a resposta é que se perdeu, não necessariamente a operação.
A partir daí só existem duas escolhas ruins: reenviar (e arriscar cobrar duas vezes) ou desistir (e arriscar não cobrar). Retry automático — que toda biblioteca de HTTP e toda fila fazem por padrão — transforma essa raridade em rotina.
O nome vem da matemática: uma operação é idempotente quando f(f(x)) = f(x). Em HTTP, GET, PUT e DELETE são idempotentes por contrato — 'apagar o recurso 42' duas vezes deixa o mundo igual. POST não é: 'criar um pagamento' duas vezes cria dois. É por isso que a chave de idempotência vive quase sempre em POSTs.
Entrega ao-menos-uma-vez não é defeito das filas — é o custo de não perder mensagem. Kafka, RabbitMQ e SQS reentregam na dúvida. A pergunta nunca é 'como evitar duplicatas na entrega' (não dá), e sim 'como tornar o processamento imune a elas'.
- Cliente
- POST + Idempotency-KeyAPI
- chave inédita?Registro de chaves
- só se for a primeira vezProcessamento
As três peças
Camada 1/3Cliente
use ← →Camada 1
Cliente
gera a chave — uma por operação, não por request
A chave nasce junto com a intenção ('pagar o pedido 8231') e se repete em todo retry daquela intenção. Pode ser um UUID guardado com o pedido ou um hash determinístico da operação. O erro clássico é gerar no momento do envio: cada retry ganha chave nova e a proteção evapora.
Se violar: chave nova a cada tentativa = servidor vê operações 'diferentes' e executa todas. A idempotência morre no cliente, silenciosamente.
Código
// A chave de idempotência transforma "cobrar" em "garantir que foi cobrado".
interface Resultado {
status: number;
corpo: unknown;
}
// chave -> resultado da primeira execução (Redis/tabela com UNIQUE na vida real)
const executadas = new Map<string, Resultado>();
async function criarPagamento(chave: string, valor: number): Promise<Resultado> {
// 1. Já vi essa chave? Devolve O MESMO resultado, sem executar de novo.
const anterior = executadas.get(chave);
if (anterior) return anterior;
// 2. Inédita: executa e guarda o resultado ANTES de responder.
// Em produção, o "guardar" precisa ser atômico (INSERT com UNIQUE):
// duas chamadas simultâneas com a mesma chave não podem passar juntas.
const cobranca = { id: crypto.randomUUID(), valor, status: "paga" };
const resultado: Resultado = { status: 201, corpo: cobranca };
executadas.set(chave, resultado);
return resultado;
}
// O cliente gera UMA chave por operação e a repete em todo retry:
const chave = "pedido-8231-pagamento"; // derivada da operação, não do request
await criarPagamento(chave, 100); // executa e cobra
await criarPagamento(chave, 100); // timeout? retry? -> mesmo resultado, R$100 uma vezimport uuid
# chave -> resultado (na vida real: tabela com UNIQUE ou SET NX no Redis)
executadas: dict[str, dict] = {}
def criar_pagamento(chave: str, valor: int) -> dict:
# repetiu a chave? devolve o mesmo resultado, sem cobrar de novo
if chave in executadas:
return executadas[chave]
cobranca = {"id": str(uuid.uuid4()), "valor": valor, "status": "paga"}
resultado = {"status": 201, "corpo": cobranca}
executadas[chave] = resultado
return resultado
chave = "pedido-8231-pagamento" # uma chave POR OPERACAO, repetida no retry
criar_pagamento(chave, 100) # cobra
criar_pagamento(chave, 100) # retry apos timeout -> mesma resposta, sem duplicarCasos de uso
Cobrança PIX com retry do integrador
Cenário
Um PaaS de pagamentos expõe POST /pix. O integrador tem retry automático com backoff, e conexões caem todo dia — timeout depois que o PIX já saiu significa segunda chamada em segundos.
Como o padrão entra
Todo POST exige Idempotency-Key. A chave é gravada com UNIQUE junto do resultado; o retry bate na constraint e recebe a mesma resposta 201 com o mesmo id de transação. O integrador pode reenviar sem medo — a API garante no máximo um PIX por chave.
Trade-off: Uma tabela a mais, com escrita no caminho crítico de toda cobrança, e a política de TTL vira contrato público: mudou o prazo, muda o comportamento do retry de todo cliente.
Consumidor de fila que reprocessa
Cenário
O worker consome 'pedido-pago' e dá baixa no estoque. A fila entrega ao-menos-uma-vez: rebalanceamento, crash antes do ack ou timeout de visibilidade fazem a mesma mensagem chegar duas vezes.
Como o padrão entra
O id do evento é a chave: a baixa registra 'evento X processado' na mesma transação que decrementa o estoque. Redelivery encontra o registro e vira no-op. O efeito acontece exatamente uma vez, mesmo com a entrega acontecendo três.
Trade-off: O registro de eventos processados cresce com o tráfego e precisa de expurgo; e a deduplicação só vale dentro da janela retida — reprocessamento de dias depois exige outra estratégia.
Armadilhas
Chave por request em vez de por operação
Se a chave é gerada na hora do envio, cada retry cria uma chave nova — e o servidor, corretamente, executa todas. A chave pertence à intenção de negócio: nasce com o pedido e sobrevive a quantas tentativas forem necessárias.
Checar e gravar em dois passos
SELECT para ver se a chave existe e INSERT depois é uma race condition clássica: duas chamadas simultâneas passam pela checagem antes de qualquer INSERT e executam as duas. A trava tem que ser uma operação atômica — UNIQUE no banco ou SET NX no Redis.
Lembrar a chave mas esquecer o resultado
Responder 'já processado' sem corpo deixa o cliente que perdeu a primeira resposta no escuro: ele precisava do id da cobrança. Idempotência de verdade devolve a mesma resposta, não um aviso de que a resposta existiu.
Confiar que 'só acontece uma vez'
Retry não é cenário raro: é o comportamento padrão de axios, fetch com retry, service meshes e toda fila. Desenhar o happy path e tratar duplicata como exceção inverte a realidade — a duplicata é parte do contrato de rede.
Quando usar × quando evitar
Use quando
3- Operações com efeito externo (dinheiro, e-mail, estoque) atrás de qualquer retry.
- Consumidores de mensageria com entrega ao-menos-uma-vez.
- APIs públicas — você não controla o comportamento de retry dos clientes.
Evite quando
3- Operações já idempotentes por natureza (PUT de estado completo, DELETE por id).
- Leituras puras — repetir um GET não tem efeito colateral.
- Fluxos internos onde a operação inteira já roda numa única transação com constraint natural (ex.: UNIQUE no par pedido+tipo).
Testar o que ficou
4 armadilhas de Idempotência viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Idempotency-Key do StripeRepetir a mesma cobrança com a mesma chave devolve o primeiro resultado em vez de cobrar de novo — a rede pode reenviar sem medo.PUT e DELETE do HTTPPor especificação, aplicar o mesmo PUT dez vezes deixa o recurso no estado que uma deixaria; é o que torna o retry seguro nesses verbos.kubectl applyAplicar o mesmo manifesto repetidas vezes reconcilia para o estado desejado sem criar cópias — o comando descreve o fim, não o passo.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Mesma chave → mesmo efeito, mesmo se repetir.
await cobrar({ idempotencyKey: key, valor });O que ele cobra
Nenhum salto a mais entre a chamada e o efeito.
- Guardar a chave de idempotência e o resultado de cada operação já processada custa armazenamento e limpeza
- Definir o que conta como 'a mesma' operação é mais sutil do que parece
Não vale se: a operação já é naturalmente idempotente, como um PUT que sobrescreve — aí adicionar controle de chave é redundante.
Onde isto já custou caro
Incidentes reais que este conceito explica.
Onde isto aparece
Continue por aqui
- WebhooksEm vez de perguntar 'já aconteceu?' de minuto em minuto, você registra uma URL e o outro sistema te chama quando acontece. É o Observer atravessando a internet — com tudo que a rede cobra por isso: assinatura, retry e duplicata.
- Race conditionDuas execuções leem o mesmo estado, decidem com base nele e gravam — e o resultado passa a depender de quem chega primeiro. É o bug que nunca aparece no teste e vende o mesmo ingresso duas vezes na sexta à noite.
- SagaCoordena uma transação distribuída de longa duração como uma sequência de transações locais, cada uma com uma ação compensatória para desfazer semanticamente em caso de falha.