Idempotency keys evitam que uma operação de escrita seja executada mais de uma vez quando o cliente repete a requisição. Elas são especialmente importantes em pagamentos, criação de pedidos, reservas, emissão de notas e qualquer ação cujo efeito duplicado cause prejuízo.
Uma requisição pode ser processada pelo servidor e a resposta se perder na rede. O cliente não sabe se deve tentar novamente. Com uma chave única, o servidor reconhece a repetição e devolve o mesmo resultado sem executar o efeito novamente.
Idempotência de métodos HTTP
GET, PUT e DELETE possuem semântica idempotente no protocolo quando implementados corretamente. POST não é idempotente por padrão. Uma idempotency key adiciona uma garantia de aplicação para uma operação específica.
Header
Idempotency-Key: 7a819f42-2f38-4d90-a97e-905f91be4478A chave deve ser imprevisível e única por intenção. UUID é uma opção comum. Não reutilize a mesma chave para operações diferentes.
Fluxo básico
- cliente gera a chave;
- servidor valida formato;
- servidor procura registro existente;
- se concluído, devolve resultado armazenado;
- se em andamento, aguarda ou retorna conflito;
- se novo, reserva a chave atomicamente;
- executa a operação;
- armazena status e resposta;
- devolve o resultado.
Middleware conceitual
async function idempotencyMiddleware(req, res, next) {
const key = req.get('idempotency-key');
if (!key || !isValidKey(key)) {
res.status(400).json({ error: 'invalid_idempotency_key' });
return;
}
const fingerprint = createFingerprint(req);
const record = await store.get(key);
if (record) {
if (record.fingerprint !== fingerprint) {
res.status(409).json({ error: 'key_reused_with_different_request' });
return;
}
if (record.status === 'completed') {
replayResponse(res, record.response);
return;
}
res.status(409).json({ error: 'request_in_progress' });
return;
}
req.idempotency = { key, fingerprint };
next();
}A reserva precisa ser atômica. Um get seguido de set sem condição permite duas execuções concorrentes.
Fingerprint da requisição
A chave deve estar associada à mesma intenção. Calcule um fingerprint com campos estáveis:
import { createHash } from 'node:crypto';
function createFingerprint(req) {
const canonical = JSON.stringify({
method: req.method,
path: req.route.path,
userId: req.user.id,
body: normalizeBody(req.body),
});
return createHash('sha256').update(canonical).digest('hex');
}Não inclua timestamps gerados pelo cliente ou ordem irrelevante de propriedades. Não armazene corpo sensível sem necessidade.
Escopo da chave
Uma chave pode ser escopada por:
- usuário;
- tenant;
- API key;
- operação;
- ambiente.
A chave composta pode ser:
tenantId:operation:idempotencyKeyIsso evita colisões entre clientes diferentes.
Reserva atômica no Redis
const reserved = await redis.set(
redisKey,
JSON.stringify({ status: 'processing', fingerprint }),
{ NX: true, EX: 3600 },
);
if (!reserved) {
// outra requisição criou o registro
}NX garante criação somente quando a chave não existe. O TTL evita registros eternos, mas deve ser maior que a duração máxima e a janela de retries.
Banco relacional
CREATE TABLE idempotency_keys (
scope VARCHAR(200) NOT NULL,
key_value VARCHAR(200) NOT NULL,
fingerprint CHAR(64) NOT NULL,
status VARCHAR(20) NOT NULL,
response_status INTEGER,
response_body JSONB,
created_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (scope, key_value)
);A restrição única fornece atomicidade. Trate conflito de inserção como repetição.
Operação e registro na mesma transação
Quando possível, grave o efeito de negócio e o resultado idempotente na mesma transação:
await database.transaction(async (tx) => {
await tx.idempotency.insertProcessing(record);
const order = await tx.orders.create(input);
await tx.idempotency.complete(key, serializeOrder(order));
});Isso reduz o risco de criar o pedido e falhar antes de registrar a resposta.
Dependência externa
Se a operação chama um provedor de pagamento, propague uma chave de idempotência aceita por ele. Ainda mantenha seu registro local para relacionar requisição, resultado e estado.
Estado processing
Duas chamadas com a mesma chave podem chegar ao mesmo tempo. Opções:
- retornar 409 ou 425;
- aguardar por curto prazo;
- assinar um canal de conclusão;
- retornar 202 com URL de status.
Evite manter conexões aguardando indefinidamente.
Falha durante processamento
Classifique falhas:
- falha antes de qualquer efeito: chave pode permitir nova tentativa;
- falha depois de efeito confirmado: preserve resultado;
- estado desconhecido: reconcilie antes de repetir;
- erro de validação: pode armazenar resposta por curto período;
- erro transitório externo: mantenha estado conforme a operação.
Resultado armazenado
Armazene somente o necessário:
- status HTTP;
- headers seguros;
- ID do recurso;
- resposta sanitizada;
- fingerprint;
- timestamps;
- estado.
Não armazene Authorization, cookies ou dados sensíveis desnecessários.
Resposta grande
Em vez de armazenar um corpo enorme, armazene o ID do recurso e reconstrua uma resposta consistente. A reconstrução deve preservar o contrato da operação original.
TTL
Escolha a retenção com base em:
- janela de retry do cliente;
- risco de duplicação;
- regras de negócio;
- volume;
- compliance;
- capacidade de armazenamento.
Pagamentos podem exigir retenção maior que operações comuns.
Limpeza
Use TTL no Redis ou job que remove registros expirados no banco. Indexe expires_at e evite uma transação gigante de limpeza.
Chave em query ou body
Prefira header. Query strings aparecem em logs e caches; corpo mistura controle de transporte com dados de negócio. Em mensagens de fila, a chave pode ficar no envelope.
Validação
Imponha:
- tamanho máximo;
- caracteres permitidos;
- entropia mínima quando aplicável;
- escopo autenticado;
- rate limit de novas chaves.
Idempotency key e segurança
Uma pessoa não deve conseguir consultar resposta de outra apenas conhecendo a chave. Sempre associe à identidade autenticada.
Reuso com payload diferente
Retorne conflito:
{
"error": "idempotency_key_conflict",
"message": "A chave já foi usada com outra requisição."
}Não execute a nova operação.
Retries do cliente
O cliente deve repetir usando a mesma chave. Gerar uma chave nova em cada tentativa elimina a proteção.
Exemplo de cliente
const idempotencyKey = crypto.randomUUID();
async function createOrder(input) {
return retry(async ({ signal }) => {
const response = await fetch('/orders', {
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': idempotencyKey,
},
body: JSON.stringify(input),
signal,
});
if (!response.ok) throw await createHttpError(response);
return response.json();
});
}Mensageria
Consumidores também precisam deduplicar mensagens. Use message ID ou event ID como chave e registre processamento atomicamente com o efeito.
Outbox
O padrão Transactional Outbox ajuda a publicar eventos sem perder a relação com a transação. Idempotência no consumidor protege contra reentrega.
Observabilidade
Meça:
- novas chaves;
- replays;
- conflitos;
- requisições em andamento;
- tempo de processamento;
- estado desconhecido;
- expirações;
- falhas do armazenamento;
- duplicações evitadas.
Não use a chave completa como label. Registre hash ou prefixo controlado apenas em logs de diagnóstico.
Testes
Teste requisições simultâneas, resposta perdida, crash após efeito, reuso com payload diferente, TTL, usuário diferente, retry e falha do Redis.
Erros comuns
- fazer get e set sem atomicidade;
- gerar chave nova em cada retry;
- não comparar fingerprint;
- armazenar segredo na resposta;
- usar TTL curto demais;
- permitir acesso entre usuários;
- marcar sucesso antes do efeito;
- repetir quando estado é desconhecido;
- não tratar concorrência;
- achar que chave substitui transação.
Fluxo recomendado
Exija chave em operações críticas, associe-a à identidade e ao fingerprint, reserve atomicamente e grave o resultado junto com o efeito. Combine com Circuit Breaker, AbortController, Rate Limiting e Fetch no Node.js.
Consulte a definição de métodos idempotentes no HTTP e a documentação do provedor utilizado para o contrato de idempotency key.



