Imagine um cliente chegando no suporte da sua loja com a cara de quem não está feliz: "Cliquei em pagar, a tela travou, cliquei de novo e fui cobrado duas vezes". Ele tem toda a razão de estar bravo. E o pior é que o código que processou o pagamento provavelmente estava "certo". Ele só não estava preparado para receber o mesmo pedido duas vezes.
Isso acontece muito mais do que a gente gostaria. Rede instável, usuário ansioso clicando no botão, servidor que demora para responder, um retry automático que alguém configurou com a melhor das intenções. Eu já vi isso em produção mais de uma vez. Numa delas, em uma integração que recebia mensagens de sistemas de saúde, o mesmo registro entrou duplicado porque a origem reenviou a mensagem achando que tinha dado erro. Passei uma sexta-feira inteira limpando dado, e prometi para mim mesmo que nunca mais ia ignorar esse assunto.
A solução tem um nome meio esquisito, mas o conceito é simples: idempotência. Resumindo bem, é a propriedade de uma operação que pode ser repetida quantas vezes você quiser sem mudar o resultado depois da primeira execução.
Neste artigo, eu vou explicar o que é idempotência com um exemplo do dia a dia, mostrar por que ela importa em APIs, pagamentos e retries, e comparar como os métodos HTTP (GET, PUT, DELETE e POST) se comportam. Depois vamos para o código: um endpoint com Express e uma idempotency key funcionando em Node.js. Para fechar, os erros mais comuns que eu vejo por aí (e que eu mesmo já cometi).
Você não precisa ter experiência com pagamentos nem com sistemas grandes. Se sabe o que é uma requisição HTTP, já dá para acompanhar.
O que é idempotência (explicado sem complicar)
A palavra assusta, eu sei. Quando ouvi pela primeira vez, lá no começo da carreira, achei que fosse coisa de matemático e deixei de lado. Só fui entender de verdade quando um bug me obrigou. Então vamos pelo caminho mais fácil: primeiro a analogia, depois a definição.
Uma analogia do dia a dia: o botão do elevador
Você chega no elevador e aperta o botão de chamar. A luz acende. Aí você fica ansioso, porque está atrasado, e aperta mais cinco vezes. O que acontece? Nada de diferente. O elevador não vem seis vezes, não chegam seis elevadores. Ele vem uma vez, exatamente como se você tivesse apertado só uma.
Agora pense num caixa eletrônico mal feito, onde cada toque no botão "sacar R$ 100" tirasse mais R$ 100 da sua conta. Apertou duas vezes, saíram R$ 200. Esse botão não é idempotente.
A diferença é essa: no elevador, repetir a ação não muda o estado final. No caixa, cada repetição muda.
A definição em uma frase
Uma operação é idempotente quando executá-la uma vez ou várias vezes seguidas deixa o sistema exatamente no mesmo estado.
Na matemática, o exemplo clássico é multiplicar por 1 ou pegar o valor absoluto de um número. Aplicou uma vez, aplicou dez, o resultado é o mesmo. Em programação, o que importa é o efeito no sistema: banco de dados, saldo, estoque, fila de mensagens.
Um exemplo bem simples em código:
// Idempotente: não importa quantas vezes eu rodar, o resultado é o mesmo
usuario.status = "ativo";
// Não idempotente: cada execução muda o resultado
usuario.pontos = usuario.pontos + 10;
A primeira linha define um valor. A segunda depende do valor anterior. Essa diferença entre "definir" e "somar" resolve metade dos problemas de idempotência que você vai encontrar.
Idempotente não é o mesmo que "não faz nada"
Aqui tem uma confusão comum. Idempotente não quer dizer que a segunda chamada é ignorada ou que dá erro. Quer dizer que o estado final do sistema é o mesmo. A resposta pode até ser diferente em detalhes (a primeira devolve "criado", a segunda devolve "já existia"), e continua sendo idempotente, desde que o efeito colateral não se repita.
Também não confunda com "seguro". Uma operação segura é a que não altera nada (um GET, por exemplo). Uma operação idempotente pode alterar, só não altera de novo na repetição. Vou voltar nisso na parte de HTTP.
Por que idempotência importa na vida real
Se tudo funcionasse sempre, ninguém precisaria disso. O problema é que sistemas distribuídos falham de jeitos criativos, e quase sempre falham no meio do caminho.
Redes falham e o cliente tenta de novo
Pense no caminho de uma requisição: o cliente manda, o servidor processa, o servidor responde. Agora imagine que a conexão caiu depois do servidor processar, mas antes da resposta chegar. Do ponto de vista do cliente, deu timeout. Ele não sabe se o servidor recebeu, se processou, se falhou. A reação natural é tentar de novo.
E aí está o problema: se a operação não for idempotente, a segunda tentativa vai executar tudo outra vez. O servidor fez o trabalho certo duas vezes, e o resultado final está errado.
Pagamentos: o caso clássico da cobrança duplicada
Pagamento é o exemplo que todo mundo entende, porque dói no bolso. O fluxo é esse: o usuário clica em "pagar", o app chama sua API, sua API chama o gateway, o gateway cobra o cartão. Se qualquer ponto dessa cadeia der timeout e alguém repetir a chamada, o cartão pode ser cobrado de novo.
É por isso que gateways sérios, como o Stripe, oferecem um header chamado Idempotency-Key. Você manda uma chave única junto com a requisição, e se mandar a mesma chave de novo, ele devolve o resultado da primeira vez em vez de cobrar outra vez. Vamos implementar algo parecido daqui a pouco.
Retries automáticos, filas e webhooks
Nem sempre é uma pessoa clicando. Muita coisa repete requisição sozinha:
- Bibliotecas de HTTP e SDKs com retry configurado.
- Filas de mensagens, que normalmente garantem entrega "pelo menos uma vez". Ou seja, a mesma mensagem pode chegar duas ou três vezes.
- Webhooks, que quem envia reenvia se você não responder 200 rápido o bastante.
- Jobs agendados que rodam de novo depois de uma queda.
Em todos esses casos, o seu código precisa aguentar receber a mesma coisa mais de uma vez. Não é opcional, é parte do contrato de viver em produção.
Uma história minha com mensagens duplicadas
Contei lá em cima, mas vale detalhar. Eu trabalhava numa integração que recebia mensagens de outros sistemas e gravava no banco. O sistema de origem tinha uma regra simples: se não recebesse confirmação em poucos segundos, reenviava. Só que o nosso lado às vezes demorava mais que isso para responder, porque gravava e ainda fazia outras coisas antes de confirmar.
Resultado: o registro entrava, a origem achava que tinha falhado, reenviava, e o registro entrava de novo. Eu só percebi quando um usuário reclamou de dado repetido na tela. Na sexta-feira daquela semana eu estava com uma query de limpeza aberta, torcendo para não apagar o registro errado.
A correção foi simples depois que entendi o problema: cada mensagem trazia um identificador, e eu passei a guardar esse identificador. Se chegasse de novo, eu reconhecia e devolvia a confirmação sem gravar nada. Dois dias de dor de cabeça resolvidos por uma tabela e um índice único. Lição aprendida.
Idempotência e métodos HTTP
O protocolo HTTP já tem essa ideia embutida. A especificação (RFC 9110) define quais métodos devem ser idempotentes. Isso significa que, se você seguir o padrão, boa parte do trabalho já vem encaminhada.
GET, PUT e DELETE: idempotentes por definição
- GET só lê. Chamar dez vezes devolve a mesma coisa (ou o dado atualizado, mas você não alterou nada).
- PUT substitui um recurso inteiro por aquilo que você mandou. Se você manda "o usuário 42 tem nome Ana" cinco vezes, o usuário 42 continua com nome Ana.
- DELETE remove um recurso. Apagar o usuário 42 uma vez ou três vezes deixa o sistema igual: o usuário 42 não existe. A segunda chamada pode devolver 404, e continua idempotente, porque o estado final é o mesmo.
Repare que a idempotência está no efeito, não no código de status da resposta.
POST: por que ele é o vilão da história
O POST normalmente serve para criar algo novo. Mandou duas vezes, criou dois. Não é idempotente por definição. Por isso, quase todo bug de cobrança duplicada, pedido duplicado ou cadastro duplicado nasce em um endpoint POST sem proteção.
Isso não é um defeito do POST. Criar coisas novas é o trabalho dele. O que a gente faz é adicionar a idempotência por cima, com uma chave enviada pelo cliente.
E o PATCH?
O PATCH é o caso cinzento. Ele altera parte de um recurso, e pode ser idempotente ou não, dependendo do que você manda. { "status": "ativo" } é idempotente. { "incrementar_pontos": 10 } não é. A RFC não promete nada sobre o PATCH, então a responsabilidade é sua.
Tabela resumo: seguro x idempotente
| Método | Seguro (não altera nada) | Idempotente |
|---|---|---|
| GET | Sim | Sim |
| HEAD | Sim | Sim |
| PUT | Não | Sim |
| DELETE | Não | Sim |
| POST | Não | Não |
| PATCH | Não | Depende |
Todo método seguro é idempotente, mas o contrário não vale. PUT e DELETE alteram o sistema e, mesmo assim, são idempotentes.
Idempotência na prática com Node.js
Chega de teoria. Vamos escrever código. Os exemplos usam Express e módulos nativos do Node, sem dependências extras, para ficar fácil de copiar e testar.
Um endpoint PUT idempotente com Express
Começando pelo caso fácil. Um PUT para criar ou substituir um usuário:
import express from "express";
const app = express();
app.use(express.json());
const usuarios = new Map();
app.put("/usuarios/:id", (req, res) => {
const { id } = req.params;
const { nome, email } = req.body;
const jaExistia = usuarios.has(id);
usuarios.set(id, { id, nome, email });
res.status(jaExistia ? 200 : 201).json(usuarios.get(id));
});
app.listen(3000);
Pode chamar esse endpoint cem vezes com o mesmo corpo. O usuário vai ser sempre o mesmo, só um registro no Map. O ID vem da URL, escolhido pelo cliente, e é isso que torna a operação idempotente: a gente não está "criando outro", está dizendo "o usuário 42 é assim".
O problema do POST de pagamento sem proteção
Agora o caso difícil. Um POST de pagamento ingênuo:
const pagamentos = [];
app.post("/pagamentos", (req, res) => {
const { valor, cartao } = req.body;
// Imagine aqui uma chamada ao gateway de pagamento
const pagamento = { id: pagamentos.length + 1, valor, cartao, status: "aprovado" };
pagamentos.push(pagamento);
res.status(201).json(pagamento);
});
Se o cliente mandar a mesma requisição duas vezes, vão existir dois pagamentos. Nada no servidor sabe que a segunda é uma repetição da primeira, porque, para ele, são duas requisições independentes.
Implementando uma idempotency key
A ideia é a seguinte: o cliente gera uma chave única (um UUID, por exemplo) e manda no header Idempotency-Key. O servidor guarda o resultado associado àquela chave. Se a mesma chave chegar de novo, devolve o resultado guardado, sem processar nada.
Vou escrever como um middleware, para reaproveitar em qualquer rota:
import { createHash } from "node:crypto";
const chaves = new Map();
function idempotencia(req, res, next) {
const chave = req.get("Idempotency-Key");
if (!chave) {
return res.status(400).json({ erro: "Envie o header Idempotency-Key" });
}
// Hash do corpo, para detectar reuso da mesma chave com dados diferentes
const hash = createHash("sha256")
.update(JSON.stringify(req.body))
.digest("hex");
const registro = chaves.get(chave);
if (registro) {
if (registro.hash !== hash) {
return res.status(422).json({
erro: "Essa chave já foi usada com um corpo diferente",
});
}
if (registro.estado === "processando") {
return res.status(409).json({
erro: "Requisição ainda em andamento, tente novamente em instantes",
});
}
// Já terminou antes: devolve exatamente a mesma resposta
return res.status(registro.statusCode).json(registro.resposta);
}
// Primeira vez que vemos essa chave
chaves.set(chave, { hash, estado: "processando", criadoEm: Date.now() });
// Intercepta o res.json para guardar a resposta antes de enviar
const jsonOriginal = res.json.bind(res);
res.json = (corpo) => {
chaves.set(chave, {
hash,
estado: "concluido",
statusCode: res.statusCode,
resposta: corpo,
criadoEm: Date.now(),
});
return jsonOriginal(corpo);
};
next();
}
E usar na rota fica assim:
app.post("/pagamentos", idempotencia, (req, res) => {
const { valor, cartao } = req.body;
const pagamento = { id: pagamentos.length + 1, valor, cartao, status: "aprovado" };
pagamentos.push(pagamento);
res.status(201).json(pagamento);
});
Três coisas importantes acontecem aí. A primeira: a mesma chave com o mesmo corpo devolve a mesma resposta, sem criar outro pagamento. A segunda: a mesma chave com um corpo diferente dá erro, porque provavelmente é um bug do cliente. A terceira: se a primeira requisição ainda está rodando, a segunda recebe um 409 em vez de executar em paralelo.
Onde guardar as chaves (memória, Redis ou banco) e por quanto tempo
Um Map em memória é ótimo para aprender e péssimo para produção. Se o servidor reiniciar, você perde tudo. Se tiver duas instâncias rodando atrás de um load balancer, cada uma tem o seu Map e a mesma chave pode passar duas vezes.
Em produção, as opções mais comuns são:
- Redis: rápido e com expiração automática. É a minha escolha na maioria dos casos. O comando
SET chave valor NX EX 86400grava só se a chave ainda não existir e já define a validade de 24 horas, tudo de forma atômica. - Banco de dados relacional: uma tabela com a chave marcada como
UNIQUE. Se duas requisições tentarem inserir a mesma chave, o banco barra uma delas. É a opção mais segura quando o resultado precisa ficar junto da transação.
Sobre o tempo de vida, não existe número mágico. O Stripe, por exemplo, apaga as chaves depois de 24 horas. Pense em quanto tempo um cliente razoável levaria para tentar de novo e coloque uma margem.
Como testar se o endpoint realmente é idempotente
Teste na mão primeiro, com curl. Rode o mesmo comando duas vezes:
curl -X POST http://localhost:3000/pagamentos \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-d '{"valor": 150, "cartao": "tok_teste"}'
A resposta da segunda vez tem que ser idêntica à da primeira, e o array de pagamentos tem que ter um único item. Depois, troque o corpo mantendo a chave e confira se vem o erro 422. Por fim, dispare duas requisições quase ao mesmo tempo (com dois terminais, ou com um script usando Promise.all) e veja se uma delas recebe o 409.
Se você quiser automatizar, vale escrever um teste que manda a mesma requisição dez vezes e verifica que só existe um efeito colateral no final.
Erros comuns ao implementar idempotência
Eu errei quase todos esses. Dá para aprender com a minha pele queimada.
Gerar a chave no servidor em vez de no cliente
Parece detalhe, mas derruba a ideia inteira. Se o servidor gera a chave, cada requisição ganha uma chave nova e nada é reconhecido como repetição. A chave precisa ser criada pelo cliente, antes do primeiro envio, e reaproveitada nas tentativas seguintes.
Reaproveitar a mesma chave para requisições diferentes
O contrário também dá problema. Um cliente que usa uma chave fixa, ou que reaproveita o mesmo UUID para compras diferentes, vai receber a resposta da primeira compra para sempre. Cada operação lógica tem a sua chave. Por isso aquele hash do corpo no middleware é útil: ele denuncia esse tipo de erro cedo.
Não tratar requisições simultâneas (race condition)
O cenário: duas requisições com a mesma chave chegam no mesmo milissegundo, e as duas leem "essa chave não existe ainda". As duas processam. Cobrança dupla, de novo.
No exemplo com Map, em uma única instância do Node, o código escapa porque a checagem e a gravação acontecem de forma síncrona. Mas com Redis ou banco, o passo "verificar e gravar" precisa ser uma operação só e atômica (o SET NX do Redis, ou a restrição UNIQUE do banco). Se você fizer um GET e depois um SET separados, está deixando uma janela aberta.
Esquecer de devolver a mesma resposta da primeira execução
Alguns desenvolvedores reconhecem a repetição e devolvem só um 200 OK genérico. Funciona até o cliente precisar dos dados da primeira resposta (o ID do pagamento criado, por exemplo). Se o cliente não recebeu a resposta original por causa do timeout, é exatamente essa resposta que ele quer na segunda tentativa. Guarde o status e o corpo, e devolva os dois.
Confundir idempotência com "ignorar duplicados" sem pensar
Descartar a requisição repetida em silêncio é tentador, e perigoso. E se a primeira falhou no meio? Você marcou a chave como usada, mas o pagamento nunca foi concluído, e agora o cliente nunca mais consegue tentar. Pense sempre no estado da operação: em andamento, concluída com sucesso, concluída com falha. Cada caso pede um tratamento. No meu middleware, se a execução falhar, vale remover a chave ou marcá-la como falha para permitir uma nova tentativa limpa.
Conclusão: idempotência como hábito
Vamos recapitular. Idempotência é a garantia de que repetir uma operação não muda o resultado depois da primeira vez. É o botão do elevador, não o caixa que cospe dinheiro a cada toque. Ela importa porque redes falham, clientes tentam de novo, filas entregam mensagem em dobro e webhooks chegam repetidos. No HTTP, GET, PUT e DELETE já nascem idempotentes, e o POST precisa de ajuda, normalmente de uma idempotency key enviada pelo cliente e guardada no servidor com cuidado.
Se eu pudesse deixar um conselho, seria esse: sempre que escrever um endpoint que altera dados, pergunte "o que acontece se isso chegar duas vezes?". Esse hábito vale mais que qualquer biblioteca. Eu faço essa pergunta até hoje, e ela já me poupou de muita sexta-feira perdida.
Agora quero ouvir você. Qual bug de duplicidade você já enfrentou? Cobrança em dobro, e-mail repetido, pedido que apareceu duas vezes? Conta aqui nos comentários, que eu leio todos e respondo.