Idempotência em APIs: o que é e por que ignorar isso vai te dar dor de cabeça
← Voltar para Codeshort

Idempotência em APIs: o que é e por que ignorar isso vai te dar dor de cabeça

Idempotência não é buzz word de tech lead. É o que separa uma API confiável de uma que duplica pedidos em produção.

DC
Dev Code Software
26 de junho de 2026·6 min de leitura

O problema que você vai enfrentar (ou já enfrentou)

O usuário clicou em "Pagar" duas vezes. A internet dele caiu no meio da requisição. O frontend disparou retry automático sem você saber. O resultado: dois pedidos criados, dois cobranças no cartão, um e-mail raivoso no suporte.

Isso não é falha do usuário. É falha de design da sua API.

Idempotência é a propriedade que garante que chamar a mesma operação uma ou dez vezes produz o mesmo resultado. Simples assim. E ignorar isso em produção é questão de quando, não de se.

O que é idempotência, de verdade

O conceito vem da matemática: uma operação é idempotente quando aplicá-la múltiplas vezes equivale a aplicá-la uma única vez.

f(f(x)) = f(x)

Traduzindo para API: se o cliente manda a mesma requisição cinco vezes, o estado do servidor deve ser idêntico ao que seria após uma única chamada. Sem duplicatas, sem efeitos colaterais acumulados.

Note que idempotência não significa que a resposta precisa ser byte a byte igual. Significa que o efeito no sistema é o mesmo.

💡 Dica: Idempotência é diferente de segurança (safety). Uma operação segura não modifica estado nenhum (como GET). Uma operação idempotente pode modificar estado, mas só na primeira chamada. DELETE é idempotente mas não é "seguro" — ele remove o recurso.

Quais métodos HTTP são idempotentes?

MétodoIdempotente?Seguro?Observação
GETLeitura pura
HEADIgual ao GET, sem body
PUTSubstitui o recurso inteiro
DELETESegundo DELETE retorna 404, mas estado é o mesmo
PATCH⚠️Depende da implementação
POSTPor padrão, cria novo recurso a cada chamada
OPTIONSMetadados do endpoint

O problema clássico mora no POST — e é onde a maioria das implementações falha.

Por que PATCH aparece como condicional? Porque PATCH /usuario/1 { "nome": "João" } é idempotente. Mas PATCH /contador/1 { "incrementar": 1 } não é — cada chamada muda o estado.

Onde a coisa quebra: POST sem idempotência

Pensa num fluxo de checkout:

// Frontend — retry automático implementado "pra ajudar o usuário"
async function criarPedido(dados) {
  for (let tentativa = 0; tentativa < 3; tentativa++) {
    try {
      const res = await fetch('/api/pedidos', {
        method: 'POST',
        body: JSON.stringify(dados),
      });
      if (res.ok) return res.json();
    } catch (err) {
      if (tentativa === 2) throw err;
      await sleep(1000);
    }
  }
}

Se a primeira chamada chegou ao servidor, processou o pagamento e criou o pedido — mas a resposta não voltou pro cliente (timeout de rede, por exemplo) — o retry vai criar um segundo pedido. O servidor não tem como saber que é a mesma operação.

Isso já custou dinheiro real em produção. Não é hipotético.

A solução: Idempotency Key

A abordagem padrão da indústria é o cliente gerar um identificador único para cada operação lógica e enviar no header. O servidor usa esse ID para deduplicar.

POST /api/pedidos
Content-Type: application/json
Idempotency-Key: 7f9e4b2a-1c3d-4e5f-8a9b-0d1e2f3a4b5c

{
  "produto_id": "prod_123",
  "quantidade": 2
}

Se o servidor já processou uma requisição com essa chave, ele devolve o resultado cacheado sem reprocessar. O cliente pode fazer retry à vontade.

Stripe, Adyen, PayPal e praticamente toda grande API de pagamento implementa isso. Não é acidente.

Implementando na prática

Veja uma implementação básica com Node.js + Express usando Redis para armazenar os resultados:

import { createClient } from 'redis';
import { v4 as uuidv4 } from 'uuid';

const redis = createClient({ url: process.env.REDIS_URL });

async function idempotencyMiddleware(req, res, next) {
  const key = req.headers['idempotency-key'];

  if (!key) {
    return res.status(400).json({
      error: 'Header Idempotency-Key obrigatório para esta operação',
    });
  }

  const cacheKey = `idempotency:${req.path}:${key}`;
  const cached = await redis.get(cacheKey);

  if (cached) {
    const { status, body } = JSON.parse(cached);
    return res.status(status).json(body);
  }

  // Intercepta a resposta para cachear antes de enviar
  const originalJson = res.json.bind(res);
  res.json = async (body) => {
    if (res.statusCode < 500) {
      await redis.setEx(
        cacheKey,
        86400, // 24 horas
        JSON.stringify({ status: res.statusCode, body })
      );
    }
    return originalJson(body);
  };

  next();
}

// Aplicando na rota de pedidos
app.post('/api/pedidos', idempotencyMiddleware, async (req, res) => {
  const pedido = await criarPedido(req.body);
  res.status(201).json(pedido);
});

Alguns detalhes que importam aqui:

TTL do cache. 24 horas é um bom padrão. Muito curto e você perde a proteção para retries tardios. Muito longo e você ocupa memória sem necessidade.

Escopo da chave. Note que o cacheKey inclui o req.path. A mesma Idempotency-Key pode ser usada em endpoints diferentes sem colisão — o que é o comportamento correto.

Erros 5xx não são cacheados. Se o servidor falhou, o retry deve tentar processar de novo. Só resultados de sucesso (ou erros de negócio como 4xx) devem ser cacheados.

⚠️ Atenção: Não use o body da requisição como chave de idempotência. Dois pedidos com o mesmo produto podem ser intencionais. A chave deve ser gerada pelo cliente para representar a intenção de operação, não o conteúdo dela.

No frontend, gere a chave antes de qualquer retry

// ❌ Errado — gera chave nova a cada tentativa
async function criarPedido(dados) {
  for (let i = 0; i < 3; i++) {
    await fetch('/api/pedidos', {
      method: 'POST',
      headers: { 'Idempotency-Key': uuidv4() }, // chave diferente!
      body: JSON.stringify(dados),
    });
  }
}

// ✅ Certo — mesma chave em todas as tentativas
async function criarPedido(dados) {
  const idempotencyKey = uuidv4(); // gerada uma vez

  for (let i = 0; i < 3; i++) {
    await fetch('/api/pedidos', {
      method: 'POST',
      headers: { 'Idempotency-Key': idempotencyKey },
      body: JSON.stringify(dados),
    });
  }
}

Parece óbvio, mas o erro de gerar a chave dentro do loop aparece mais do que deveria em code review.

Erros comuns que devs cometem

1. Implementar idempotência só em pagamentos. Criação de usuários duplicados, notificações enviadas em dobro, webhooks processados duas vezes — qualquer operação com efeito colateral precisa de proteção.

2. Assumir que PUT é sempre seguro. PUT é idempotente por spec, mas se sua implementação faz alguma coisa diferente na segunda chamada (auditoria, trigger, evento), você quebrou a propriedade.

3. Não validar se a chave pertence ao mesmo usuário. Se o cliente A manda uma Idempotency-Key igual à que o cliente B já usou, o servidor pode devolver dados do cliente B. Inclua sempre o ID do usuário no escopo da chave no servidor:

const cacheKey = `idempotency:${req.user.id}:${req.path}:${key}`;

4. Não documentar que o endpoint exige a chave. O contrato da API precisa deixar claro quais endpoints exigem Idempotency-Key. Se for opcional, documente o comportamento padrão sem ela.

FAQ

Preciso implementar idempotência em GET? Não. GET já é idempotente por natureza — ele não modifica estado. O que você precisa garantir é que GET realmente não causa efeito colateral. Se seu GET incrementa um contador de visualizações, por exemplo, você tem um design problem mais sério.

Posso usar o timestamp como Idempotency-Key? Não é uma boa ideia. Timestamps não são únicos o suficiente em sistemas distribuídos ou com múltiplas tabs abertas. Use UUID v4 ou nanoid. São gerados no cliente, sem necessidade de coordenação.

Quanto tempo devo guardar o resultado no cache? Depende do SLA da sua operação. Para pagamentos, 24h é o padrão da Stripe. Para criação de recursos simples, 1h já costuma ser suficiente. O que define isso é: qual é o máximo de tempo que um retry legítimo pode levar?

E se dois requests com a mesma chave chegarem simultaneamente (race condition)? Você precisa de um lock distribuído. Com Redis, use SET NX (set if not exists) para criar um lock antes de processar. Isso garante que apenas um processamento ocorre, mesmo com requisições paralelas.

const lock = await redis.set(
  `lock:${cacheKey}`,
  '1',
  { NX: true, EX: 30 } // expira em 30s
);

if (!lock) {
  return res.status(409).json({ error: 'Requisição em processamento' });
}

PATCH é idempotente ou não? Depende do que o PATCH faz. Se substitui um campo por um valor fixo ("status": "ativo"), é idempotente. Se acumula ("creditos": "+10"), não é. Você decide na implementação — mas documente o comportamento.

Próximos passos

Se você tem uma API em produção que aceita POST sem nenhuma proteção de deduplicação, esse é o lugar certo para começar:

  1. Mapeie os endpoints críticos — pagamentos, criação de pedidos, envio de notificação, qualquer coisa com efeito colateral irreversível.
  2. Adicione o middleware de idempotência com Redis. O exemplo acima funciona em produção com pequenas adaptações.
  3. Atualize o frontend para gerar a Idempotency-Key antes dos loops de retry — e manter a mesma chave em todas as tentativas.
  4. Documente no contrato da API quais endpoints exigem a chave e qual o TTL esperado.
  5. Leia a RFC 9110 para entender a semântica oficial dos métodos HTTP — é mais curta do que parece.

Sua API vai lidar com rede instável, usuário ansioso clicando duas vezes e retry automático do SDK. Idempotência não é otimização. É fundação.