Idempotência de API: evite criações duplicadas com um design seguro para re-tentativas
A idempotência de API ajuda a evitar criações duplicadas e cobranças em dobro usando IDs de requisição, restrições de unicidade e regras seguras de re-tentativa para seus endpoints.

Por que criações duplicadas acontecem na vida real
Criações duplicadas geralmente não são culpa de “usuários maus”. Acontecem porque redes reais e pessoas reais são instáveis.
Um celular em Wi‑Fi instável envia uma requisição, o app mostra um spinner, e o usuário toca no botão novamente. Ou a requisição chega ao servidor, mas a resposta não volta, então o cliente re-tenta.
Re-tentativas também acontecem automaticamente. Apps móveis, bibliotecas de fetch do navegador, gateways de API e executores de jobs em background costumam re-tentar após um timeout, uma conexão perdida ou um erro 502/503. Do ponto de vista do cliente, re-tentar é a escolha segura: "Não tenho certeza se funcionou, então vou tentar de novo."
Na prática, uma "criação duplicada" se parece com isto:
- Dois pedidos criados para um checkout
- Dois tickets de suporte para uma reclamação
- Duas assinaturas ou cobranças por um clique
- Dois convites enviados para a mesma pessoa
O assustador é que o servidor muitas vezes não consegue distinguir entre uma requisição nova e a mesma requisição repetida. Se seu endpoint POST sempre insere uma nova linha, cada re-tentativa vira uma segunda linha. O cliente pode nem mostrar isso, mas seu banco de dados e seus clientes vão.
Backends gerados por IA têm ainda mais probabilidade de perder isso porque tendem a focar no caminho feliz: um clique, uma requisição, uma resposta. Frequentemente não modelam timeouts, taps duplos ou requisições em corrida.
O objetivo é simples: se a mesma requisição de criação for repetida, ela deve produzir o mesmo resultado, não uma segunda ação. É isso que a idempotência de API protege.
Idempotência de API em termos simples
Idempotência de API significa: se a mesma requisição for enviada mais de uma vez, o resultado é o mesmo que se ela fosse enviada uma vez.
Isso importa porque muitos sistemas, na prática, funcionam com entrega “pelo menos uma vez”. Uma requisição pode chegar uma vez ou duas vezes, mas normalmente ela chega.
Algumas ações são naturalmente idempotentes. Um GET típico pode ser repetido sem alterar nada. Atualizar uma página não deveria criar um novo usuário.
Outras ações não são naturalmente idempotentes. Um POST típico que cria algo (um pedido, um usuário, uma cobrança) pode rodar duas vezes e produzir dois registros ou duas cobranças. Se o primeiro POST teve sucesso mas a resposta foi perdida, o cliente re-tenta e cria acidentalmente um duplicado.
Idempotência te dá uma maneira segura de re-tentar. Não previne toda falha, e não corrige lógica de negócio quebrada, mas evita ações duplas quando re-tentativas ocorrem.
Onde a idempotência importa mais
Você não precisa de idempotência em todo lugar. Precisa onde uma re-tentativa pode acionar uma segunda ação no mundo real: mais dinheiro cobrado, mais e-mails enviados, mais jobs iniciados, mais linhas criadas.
Priorize endpoints que criam ou disparam algo difícil de desfazer. Exemplos comuns:
- Pagamentos (charge, authorize, capture)
- Pedidos, assinaturas, reservas
- Envio de email/SMS (reset de senha, convites, recibos)
- Jobs em background (exports, processamento, geração de relatórios)
- Handlers de webhook que criam ou concedem algo
Também fique atento a endpoints que parecem “atualizações” mas se comportam como adições. Qualquer coisa que incremente contadores, anexe itens, aplique créditos ou altere totais pode ser aplicada em dobro nas re-tentativas.
Chaves de idempotência (IDs de requisição): o padrão básico
Chaves de idempotência são a forma mais prática de tornar requisições de escrita seguras para re-tentativas.
A ideia é simples: o cliente envia um ID de requisição único com uma requisição de escrita (normalmente um POST) e o servidor promete que repetir a mesma chave não criará um segundo recurso nem repetirá a ação.
Uma abordagem comum é aceitar um cabeçalho como Idempotency-Key (ou um campo requestId no corpo). Se o cliente der timeout e re-tentar, ele reutiliza a mesma chave.
No servidor, você guarda um pequeno registro por chave. A maioria das equipes mantém:
- a chave
- a quem ela pertence (escopo)
- a qual operação se aplica (endpoint, método)
- status (in_progress, succeeded, failed)
- a resposta que foi retornada (código de status e corpo)
Escopo importa
Uma chave não deve ser global em todo o sistema. Bons padrões a vinculam a uma destas opções: por usuário, por conta/tenant, ou por token de API, e normalmente por endpoint.
Por exemplo, uma chave usada para POST /orders não deveria ser aceita para POST /refunds, mesmo que a string bata.
Retenção é um tradeoff
Mantenha chaves tempo suficiente para cobrir re-tentativas reais (minutos a horas), mais uma margem para clientes lentos e filas. Algumas equipes as mantêm por 24 horas ou alguns dias para proteger contra replays acidentais. Retenção mais longa reduz duplicatas, mas aumenta armazenamento e exige plano de limpeza.
Comportamento em replay
Quando a mesma chave é re-enviada, o servidor deve retornar o resultado original, não executar a ação de novo.
Exemplo: um checkout retorna 201 com order_id=123. Se o cliente re-tentar com a mesma chave, retorne o mesmo 201 e o mesmo order_id.
Restrições de unicidade: a rede de segurança que pega condições de corrida
Chaves de idempotência ajudam, mas não são suficientes sozinhas. Sua última linha de defesa é o banco de dados.
Quando duas requisições atingem sua API quase ao mesmo tempo, só o banco pode de forma confiável impedir que ambas criem a mesma coisa.
Uma restrição de unicidade diz ao banco: “só pode existir uma linha assim”. Mesmo se seu código rodar duas cópias da requisição em paralelo, o banco rejeita o duplicado.
Exemplos comuns:
- Email único em uma tabela
users - Par único como
(account_id, external_id)ao importar do Stripe, QuickBooks ou um CRM order_numberúnico
Um padrão prático é armazenar a chave de requisição no registro que você cria. Por exemplo, adicione uma coluna idempotency_key em orders e torne-a única, frequentemente com escopo tipo (account_id, idempotency_key). Então toda re-tentativa mapeia para a mesma linha.
Quando a restrição é acionada, sua API geralmente faz uma de duas coisas:
- Retorna o registro existente (melhor para “criar pedido”, “iniciar checkout”, “criar fatura”)
- Retorna um erro claro de conflito (melhor quando reusar uma chave pode ocultar um erro real)
Não confie em “checar então inserir” no código da aplicação. Dois workers podem ambos checar “existe?” e ver “não” antes de qualquer um inserir. Faça a unicidade uma regra do banco.
Como adicionar idempotência a um endpoint POST (passo a passo)
Se um cliente der timeout e re-tentar um POST, você quer que a segunda chamada retorne o mesmo resultado, não crie um segundo registro.
Comece com duas decisões:
- Quais ações causam dano real quando repetidas (pedidos, cobranças, convites, jobs)
- Qual o escopo da chave (por endpoint + usuário/conta é um bom padrão)
Depois implemente de modo a permanecer correto sob concorrência.
1) Exija um ID de requisição estável
Para endpoints arriscados, exija um cabeçalho Idempotency-Key (ou um campo de request ID). Clientes devem reutilizar o mesmo valor nas re-tentativas.
2) Persista a chave
Ou crie uma tabela de idempotência (key, endpoint, user/account, status, response), ou armazene a chave diretamente no registro criado quando houver um mapeamento 1:1 limpo.
3) Reivindique a chave de forma atômica
Insira o registro da chave primeiro, protegido por uma restrição única (ou adquira um lock). É assim que você evita que duas requisições concorrentes “ganhem”.
4) Armazene a resposta que você retorna
Depois que a ação for bem‑sucedida, armazene a resposta (código de status e corpo). Em repetições, retorne essa resposta armazenada sem reexecutar o trabalho.
5) Decida o que fazer quando uma requisição estiver em progresso
Se uma re-tentativa chegar enquanto a primeira tentativa ainda está rodando, você precisa de comportamento previsível. Opções comuns são:
- esperar brevemente e re-checar
- retornar
409 Conflict(ou202 Accepted) com uma mensagem clara de “ainda sendo processado”
Escolha uma abordagem e mantenha-a consistente.
Lidando com casos complicados: timeouts, concorrência e falhas parciais
Re-tentativas ficam confusas quando cliente e servidor discordam sobre o que aconteceu. Idempotência torna esses momentos sem graça: mesma requisição, mesmo resultado.
Timeout após sucesso
O servidor criou o registro, mas a resposta nunca chegou. Sem proteção, a re-tentativa cria um segundo registro.
Trate a chave de idempotência como o recibo. Se a re-tentativa usar a mesma chave, retorne o resultado original.
Crash no meio e re-tentativas concorrentes
Armazenar a chave não basta se você não rastrear o que aconteceu.
Um conjunto de regras prático:
- Armazene status: pending, succeeded, failed.
- Garanta que apenas uma requisição possa ser dona da chave (restrição única é a guarda mais simples).
- Se uma re-tentativa encontrar pending, não inicie uma segunda execução. Espere brevemente ou retorne uma resposta clara de “ainda processando”.
- Após sucesso, sempre retorne a mesma resposta para a mesma chave enquanto ela for retida.
Falhas parciais
Este é o caso mais difícil. Você pode criar um usuário mas falhar ao enviar o email de boas‑vindas, ou capturar o pagamento mas não criar a linha do pedido.
Escolha uma “fonte de verdade” clara para a requisição e garanta que re-tentativas não repitam efeitos colaterais que já sucederam. Muitas vezes isso significa:
- terminar os passos restantes assincronamente
- usar um passo de compensação (por exemplo, reembolsar um pagamento quando o pedido não pôde ser criado)
Erros comuns que ainda deixam duplicatas acontecerem
A maioria das criações duplicadas não acontece por falta total de idempotência. Acontece porque a idempotência foi adicionada pela metade.
Modos comuns de falha:
- Tornar a chave de idempotência opcional, então apenas algumas requisições ficam protegidas.
- Salvar a chave mas não salvar o resultado, então a re-tentativa ainda reexecuta o trabalho.
- Não escopar chaves (uma chave reutilizada entre usuários ou endpoints pode colidir).
- Fazer “checar então inserir” sem uma restrição única no banco.
- Tratar efeitos colaterais como “enviar email” como idempotentes sem rastrear se foram enviados.
Um cenário clássico é POST /orders que cobra o cartão primeiro, então trava antes de retornar uma resposta. Se a re-tentativa cobrar de novo, você terá uma cobrança em dobro. Evite isso persistindo o resultado e protegendo-o com unicidade.
Checklist rápido para verificar comportamento seguro a re-tentativas
Uma API segura para re-tentativas deve se comportar do mesmo jeito toda vez que a mesma ação for repetida.
Para endpoints de escrita (POST, e às vezes PATCH/DELETE):
- Exija um
Idempotency-Keypara operações que criam ou disparam algo. - Aplique uma restrição única no banco para a regra de deduplicação.
- Verifique se re-tentativas retornam o mesmo ID de recurso e o mesmo corpo de resposta (ou a mesma representação estável).
- Defina o escopo da chave (por usuário/conta + por endpoint é uma boa linha de base).
- Mantenha chaves tempo suficiente para cobrir re-tentativas reais, e registre contexto suficiente para depurar.
Para handlers de webhook:
- Trate o ID de evento do provedor como a chave de idempotência, armazene-o e retorne sucesso em repetições.
Um teste simples é enviar exatamente a mesma requisição cinco vezes rapidamente (incluindo a mesma chave). Você deve ver uma criação e quatro respostas de “mesmo resultado”.
Exemplo: prevenindo cobranças duplas em um checkout instável
Um fundador solo está testando um checkout que começou como um protótipo gerado por IA. Funciona nas demos, mas no uso real a rede é instável e a UI às vezes fica presa no spinner.
Um cliente clica em "Pagar" uma vez. Nada parece acontecer. Ele clica de novo. As duas requisições chegam à API.
Sem idempotência, o backend trata isso como duas compras separadas. Você pode acabar com dois pagamentos bem‑sucedidos, duas linhas de pedido, e um ticket de suporte que começa com: "Eu só cliquei uma vez." O cliente pode até abrir uma disputa porque não consegue saber qual cobrança é válida.
Com uma chave de idempotência mais uma restrição única no banco, a segunda requisição não cria nada novo. O servidor a reconhece como replay e retorna o mesmo resultado da primeira chamada: o ID do pedido original e o status do pagamento.
Normalmente se parece com isto:
- O cliente gera uma única idempotency key quando o usuário aperta "Pagar"
POST /checkoutarmazena essa chave com a tentativa de pedido- O banco impõe unicidade sobre
(user_id, idempotency_key)ou(merchant_id, idempotency_key) - Na re-tentativa, a API busca o registro existente e o retorna
O suporte fica mais fácil se você registrar alguns campos: idempotency key, order ID resultante, charge ID do provedor de pagamento, timestamp e se a requisição foi um replay.
Próximos passos para APIs geradas por IA que quebram com re-tentativas
Backends gerados por IA muitas vezes parecem ok numa demo, então quebram quando usuários atualizam, redes móveis caem ou provedores re-tentam webhooks. Estabilize os endpoints que podem causar danos reais quando rodarem duas vezes.
Escolha suas três operações de maior risco (geralmente pagamentos, pedidos, convites e webhooks recebidos). Adicione duas camadas:
- Chaves de idempotência para tornar re-tentativas seguras de propósito
- Restrições únicas no banco para pegar condições de corrida
Depois faça um teste pequeno e realista: clique duas vezes no botão enviar, simule um timeout do cliente e re-tentativa com o mesmo request ID, e dispare duas requisições concorrentes com o mesmo payload. Você quer uma criação real e respostas consistentes de “mesmo resultado”.
Se você herdou um código gerado por IA e duplicatas já estão acontecendo, muitas vezes é mais rápido fazer remediação focada do que uma reescrita completa: corrija um endpoint de ponta a ponta, depois vá para o próximo. Se quiser uma segunda opinião, FixMyMess (fixmymess.ai) se especializa em diagnosticar e reparar backends gerados por IA, incluindo adicionar idempotência, salvaguardas de unicidade e comportamento de re-tentativa pronto para produção.
Perguntas Frequentes
Por que estou vendo registros duplicados mesmo quando os usuários juram que clicaram apenas uma vez?
Porque redes e apps fazem re-tentativas. Uma requisição pode ter sido processada no servidor, mas a resposta se perdeu, ou o usuário deu dois toques enquanto a interface estava travada. Se seu POST sempre insere uma nova linha, cada re-tentativa pode se tornar uma segunda criação.
O que “idempotência de API” significa em termos simples?
Idempotência significa que repetir a mesma requisição produz o mesmo resultado que enviá-la uma vez. Para ações de criação, isso normalmente significa que a mesma entidade é retornada na re-tentativa em vez de criar uma nova.
Quais endpoints devo tornar idempotentes primeiro?
Use em ações onde uma re-tentativa pode causar um efeito real que você não quer duplicado — por exemplo, cobrar dinheiro, criar pedidos, enviar convites ou iniciar jobs longos. Normalmente não é necessário para leituras, e pode não ser necessário para atualizações que não causam efeitos irreversíveis.
O que é uma chave de idempotência e como ela é usada?
Uma chave de idempotência é um ID de requisição gerado pelo cliente enviado com uma requisição de escrita, frequentemente via cabeçalho Idempotency-Key. O servidor registra essa chave e, se vir a mesma novamente, retorna o resultado original em vez de reexecutar a ação.
Como devo escopar chaves de idempotência para que não colidam?
Escopo ao ator e à operação. Um bom padrão é por conta/usuário mais endpoint e método, assim a mesma string não acaba sendo aplicada acidentalmente a um usuário diferente ou a outra ação, como reembolsos.
Por quanto tempo devo armazenar chaves de idempotência?
Guarde por tempo suficiente para cobrir re-tentativas reais e replays atrasados — tipicamente horas até um dia para muitos produtos. Retenção maior reduz o risco de duplicatas, mas aumenta uso de armazenamento e exige limpeza; escolha uma janela que você possa aplicar consistentemente.
O que o servidor deve retornar quando recebe a mesma chave de idempotência novamente?
Retorne a resposta original para aquela chave, incluindo mesmo código de status e corpo, e não repita o efeito colateral. Isso torna as re-tentativas do cliente seguras e previsíveis, especialmente após timeouts ou conexões perdidas.
Por que ainda preciso de uma restrição única no banco se tenho chaves de idempotência?
Trate a restrição única do banco como a guarda final contra condições de corrida. Duas requisições podem passar no padrão “checar então inserir” ao mesmo tempo, mas o banco pode impor “apenas uma linha assim”, permitindo que você busque e retorne o registro existente em caso de conflito.
O que acontece se uma re-tentativa chegar enquanto a primeira requisição ainda está rodando?
Você precisa de uma regra clara para chaves “pendentes” para não executar a ação duas vezes. Abordagens comuns são esperar brevemente e re-checar, ou retornar uma resposta clara de “ainda processando”; depois garanta que o resultado final armazenado seja o que as re-tentativas receberão.
Como posso testar rapidamente se minha API é segura para re-tentativas?
Envie exatamente a mesma requisição várias vezes com a mesma chave de idempotência e confirme que você tem uma criação e respostas repetidas consistentes. Se você herdou um backend gerado por IA que quebra com re-tentativas, uma remediação focada costuma ser mais rápida; a FixMyMess pode auditar endpoints de risco e adicionar idempotência mais restrições de banco de dados de ponta a ponta.