Prevenir envios duplicados: cliques seguros sem confusão
Evite envios duplicados com estados de UI claros, tokens de requisição e verificações no servidor para que usuários não provoquem cobranças duplicadas ou ações repetidas.

O que dá errado quando um botão é clicado duas vezes
Um duplo clique raramente é intencional. Mais frequentemente a página parece lenta, o botão não reage visivelmente e as pessoas clicam de novo para ter certeza de que funcionou. No mobile, um toque pode registrar duas vezes se a UI travar.
O problema é simples: muitos cliques em botões disparam efeitos colaterais. Se seu app tratar cada clique como uma ação totalmente nova, você pode executar o mesmo efeito duas vezes mesmo que o usuário só quisesse uma vez.
Resultados comuns:
- Dois pedidos criados para o mesmo carrinho
- Duas tentativas de pagamento para a mesma fatura
- Dois e-mails de confirmação (ou SMS) enviados
- Linhas duplicadas no banco de dados (usuários, convites, tickets)
- Uma ação de “criar” que roda duas vezes e quebra uma etapa posterior
Isso é problema de UX e de integridade de dados. Usuários veem sequências confusas como “Sucesso” seguido por um erro, ou são cobrados duas vezes e perdem confiança rapidamente. Sua equipe então precisa cuidar de reembolsos, mesclar registros e responder tickets de suporte.
Redes lentas pioram a situação. Uma requisição pode ser enviada com sucesso, mas a resposta chegar atrasada, então a UI ainda parece inativa. Alguns usuários atualizam a página, reabrem o app ou tentam de novo, o que cria os mesmos efeitos duplicados de um duplo clique.
O objetivo não é apenas bloquear cliques extras. As pessoas devem receber um feedback claro de que algo está acontecendo, e o sistema deve ser seguro caso o usuário tente novamente, atualize a página ou a requisição seja repetida.
Quais ações precisam de proteção (e quais geralmente não precisam)
Um “efeito colateral” é qualquer coisa que seu app faz que muda o mundo fora da tela: criar um registro, cobrar um cartão, enviar um e-mail ou SMS, atualizar uma senha, reservar estoque.
Ações “seguras” costumam ser leituras. Carregar uma página, buscar, ordenar, abrir um modal ou atualizar um dashboard pode ser repetido sem dano real. Pode ser irritante se piscar, mas não cria consequências duradouras.
As ações que mais frequentemente precisam de proteção são aquelas que criam ou finalizam algo, movem dinheiro ou créditos, enviam mensagens, mudam acessos ou disparam integrações a jusante.
Gatilhos ocultos de repetição são comuns. Pessoas pressionam Enter em um formulário, dão duplo toque no mobile quando a UI está lenta, ou clicam de novo porque o botão não mostrou um estado de trabalho claro. Browsers e camadas de rede também podem reproduzir requisições após uma queda temporária.
Pode até acontecer com um só clique: uma requisição expira, o usuário atualiza e tenta de novo, ou a rede entrega a mesma requisição duas vezes. A mentalidade mais segura é simples: qualquer ação com efeito colateral deve assumir que duplicatas são possíveis e tratá-las com elegância.
Padrões de UI que evitam confusão enquanto bloqueiam repetições
Envios duplos frequentemente acontecem porque a interface não reconheceu claramente o primeiro clique. A correção mais rápida é bloquear cliques repetidos. A correção melhor é bloquear repetições enquanto deixa óbvio que há trabalho em progresso.
Desabilite o botão no momento do clique e faça o estado desabilitado parecer intencional, não quebrado. Associe isso a uma mudança de rótulo como "Salvando..." ou "Finalizando pedido...". Se a ação falhar, retorne o botão ao normal e mostre uma mensagem de erro que diga ao usuário o que fazer em seguida.
Mantenha o layout estável. Se o texto do botão mudar de tamanho e a UI deslocar, um segundo clique pode cair em outro elemento. Reserve espaço para o rótulo de carregamento ou mantenha a largura do botão constante.
Pequenos sinais de UI que reduzem cliques repetidos
Alguns sinais pequenos ajudam muito:
- Mude o rótulo para um status curto ("Salvando..."). Use um spinner só se não causar saltos no layout.
- Mantenha o botão do mesmo tamanho e no mesmo lugar, mesmo quando desabilitado.
- Para ações mais longas, adicione uma linha auxiliar como "Isto pode levar até 20 segundos.".
- Se houver etapas, mostre texto de progresso ("Etapa 2 de 3").
Para ações que levam mais que um ou dois segundos, defina expectativas desde o começo. Uma mensagem curta costuma ser melhor que um spinner vago. Se você pode estimar tempo, seja honesto e arredonde para cima.
Salvaguardas no cliente: desabilitar, debounce e bloqueio em voo
A maioria dos envios duplicados começa do mesmo jeito: a UI continua aceitando cliques enquanto a primeira requisição ainda está em execução. Sua primeira linha de defesa é um estado pendente claro que bloqueia repetições sem deixar o app parecer travado.
Um bom estado pendente tem duas funções: impedir cliques extras e mostrar ao usuário que algo está acontecendo. Se você apenas desabilitar o botão sem feedback, as pessoas vão clicar em outro lugar ou atualizar a página.
Um padrão prático no cliente:
- Defina uma flag
pending = trueimediatamente no clique. - Desabilite o botão e mostre um rótulo de carregamento.
- Ignore cliques adicionais enquanto
pendingfor true (não os enfileire). - Reative somente no sucesso, ou em um estado de falha conhecido que você possa explicar.
- Sempre limpe
pendingem umfinallypara que erros não bloqueiem a UI.
Debounce é diferente. Desabilitar bloqueia repetições durante uma requisição em voo. Debounce filtra eventos de disparo rápido (como um duplo toque no trackpad) dentro de uma janela curta, como 250 a 500 ms. Use-o como proteção leve, não como substituto de um gerenciamento de estado apropriado.
Respostas lentas e respostas instantâneas devem se comportar da mesma forma. Mesmo que uma chamada à API retorne em 50 ms, mantenha o fluxo consistente: mostre um breve estado pendente e então confirme o sucesso. Do contrário, os usuários aprendem “às vezes funciona instantâneo, às vezes não” e começam a clicar duas vezes por via das dúvidas.
Tokens de requisição e cancelamento: quando ajudam e quando não ajudam
Cancelar requisições soa como algo que impediria duplicatas, mas geralmente significa algo mais restrito: o app para de ouvir uma resposta antiga. A chamada de rede pode ainda terminar, mas sua UI a ignora porque o usuário seguiu adiante.
Isso é mais útil quando a intenção mais recente deve vencer. Pense em caixas de busca, filtros, abas e scroll infinito. Se respostas antigas ainda puderem atualizar a tela, a UI pode piscar ou mostrar resultados errados.
Quando o cancelamento ajuda
O cancelamento é uma rede de segurança de UX quando:
- O usuário navega embora e você não quer que uma resposta tardia atualize a página anterior.
- O usuário muda filtros rapidamente e só os resultados mais novos devem renderizar.
- O usuário digita texto de busca e consultas antigas devem ser ignoradas.
- Você dispara requisições em background ao rolar e quer parar trabalho quando a lista não está mais visível.
Um bug comum é “resposta obsoleta sobrescreve estado fresco”, especialmente quando múltiplos fetches competem. Cancelamento junto com uma verificação simples do tipo “aplicar resposta só se o token bater com a requisição atual” costuma corrigir isso.
Quando o cancelamento não resolve duplicatas
Cancelamento não evita duplicatas de forma confiável. Se o usuário der duplo clique em “Pagar” e duas requisições chegarem ao servidor, o servidor ainda pode processar ambas. Cancelar a segunda requisição no cliente pode acontecer tarde demais, e cancelar a primeira não desfaz o trabalho que já ocorreu.
Para evitar uma UI confusa, trate requisições canceladas como neutras. Elas não devem tirar o botão do estado de carregamento de volta para pronto, e não devem mostrar um toast de erro tipo “Pagamento falhou” quando o pagamento realmente foi concluído.
Se você precisa de proteção real contra duplicatas para ações críticas, use cancelamento para manter a UI precisa, mas confie em idempotência no servidor para impedir efeitos duplos.
Idempotência no servidor: a maneira confiável de parar duplicatas
Truques de UI ajudam, mas o único lugar onde você pode realmente impedir envios duplicados é no servidor. Redes fazem retry, usuários atualizam, e apps móveis reenviam requisições. Se seu backend tratar cada requisição como “nova”, duplicatas passarão.
Uma chave de idempotência é um recibo único para uma ação pretendida. O cliente a envia com a requisição (frequentemente em um header) e o servidor registra que já processou exatamente aquela ação. Se a mesma chave aparecer de novo, o servidor não executa o efeito colateral outra vez. Ele retorna o mesmo resultado que retornou na primeira vez.
Como usar uma chave de idempotência
Um fluxo prático:
- Gere uma chave única por ação (por exemplo, por tentativa de checkout).
- Envie-a com a requisição e armazene-a com a resposta final.
- Em uma requisição repetida com a mesma chave, retorne a resposta armazenada.
- Expire chaves após uma janela curta que cubra retries realistas.
Chaves geradas pelo cliente funcionam bem quando usuários podem tentar de novo após um refresh, navegação back/forward ou Wi‑Fi instável. Chaves geradas pelo servidor também funcionam, mas só se o cliente puder reutilizar a mesma chave nas tentativas.
Mantenha as chaves ativas tempo suficiente para cobrir retries realistas (minutos a um dia é comum), mas não para sempre. Armazene-as em lugar durável; caches somente em memória podem falhar durante reinícios.
Verificações no banco de dados e regras de negócio que reforçam sua UI
Mesmo que sua UI pareça perfeita, duplicatas ainda podem acontecer. O lugar mais seguro para impedir repetições é o banco de dados e as regras de negócio mais próximas dele.
Comece bloqueando duplicatas na fonte com uma constraint única. Em vez de esperar que seu código só crie uma linha, torne impossível inserir uma segunda. Exemplos comuns incluem um número de pedido único, um payment intent ID único ou um par único como (user_id, request_id).
Também garanta que seu código seja seguro para concorrência. Um bug clássico é “verificar então criar”: o app verifica se existe um registro, não vê nada e então cria. Sob carga, duas requisições podem executar essa verificação simultaneamente e ambas criam uma linha. Coloque a verificação e a criação dentro de uma única transação, ou use um upsert para que apenas uma vença.
Algumas proteções que valem a pena:
- Constraints únicas para registros de uso único (pedidos, cadastros, reset de senha, intents de pagamento)
- Transações (ou upsert) para que duas requisições concorrentes não passem pelo mesmo gatilho
- Um campo de status (pending, completed, failed) com transições permitidas
- Logs e alertas sobre tentativas duplicadas para detectar padrões cedo
Quando uma duplicata é bloqueada, retorne uma resposta previsível que a UI possa traduzir em texto amigável, como: “Este pedido já foi criado. Mostrando seu recibo.” Evite erros assustadores que façam o usuário clicar de novo.
Fluxos de pagamento merecem cuidado extra. Nunca crie duas cobranças para a mesma intenção. Trate a intenção como um objeto de negócio único, imponha-a com uma chave única e faça a etapa de cobrança rodar uma vez mesmo que o cliente tente novamente.
Casos reais de borda que ainda causam envios duplicados
Mesmo se você desabilitar o botão e mostrar um spinner, duplicatas podem escapar. Muitos envios duplos acontecem sem um segundo clique óbvio.
Rede lenta é o caso clássico. Se a UI fica quieta por um ou dois segundos, as pessoas tocam de novo, especialmente no mobile. Timeouts pioram: a primeira requisição pode completar no servidor enquanto o navegador mostra um erro e convida ao retry.
Outros casos comuns muitas vezes parecem comportamento do usuário, mas frequentemente são do browser ou da rede:
- Refresh ou Back/Forward podem reproduzir um envio de formulário.
- Múltiplas abas ou dispositivos podem confirmar a mesma ação em paralelo.
- Retries automáticos do SO, bibliotecas HTTP, proxies ou gateways podem reproduzir requisições.
- Uma resposta perdida pode fazer o usuário tentar de novo mesmo que o servidor já tenha tido sucesso.
Um exemplo realista: um usuário toca em "Pagar", a rede trava e ele vê um erro genérico. Ele toca em "Pagar" de novo. Ambas as requisições chegam ao seu servidor e você cria dois pedidos e cobra duas vezes. Do ponto de vista do usuário, ele fez exatamente o que qualquer pessoa razoável faria.
Trate a UI como uma dica útil, não como rede de segurança. Faça o sucesso ser seguro para repetir com uma regra de idempotência no servidor e retorne o resultado original em repetições.
Erros comuns que criam duplicatas (ou quebram a UX)
Desabilitar um botão é um bom começo, mas não é suficiente. Se a requisição é lenta, a página atualiza ou o usuário abre uma segunda aba, esse estado desabilitado desaparece e a ação pode disparar de novo.
Outra armadilha é confiar em um timer front-end como “debounce 500ms.” Isso apenas bloqueia cliques rápidos, não repetições do mundo real. Um usuário pode clicar, esperar dois segundos, não ver nada e clicar de novo. Se a primeira requisição ainda estiver em voo, você pode criar dois pedidos, convites ou cobranças.
Falha parcial é onde times se queimam. O servidor pode ter sucesso, mas a UI mostra erro por timeout, conexão perdida ou crash do app. O usuário tenta novamente. Sem uma forma no servidor de reconhecer “isto é a mesma ação”, o retry vira duplicata.
Tokens ajudam, mas somente se forem realmente únicos por operação e com escopo correto. Problemas aparecem quando um token é reutilizado entre ações diferentes, ou quando não é único por tentativa. Aí você acaba permitindo duplicatas ou bloqueando a requisição errada.
Uma mentalidade mais segura: deixe a UI reduzir repetições acidentais e deixe o servidor decidir se uma ação é nova ou um retry.
Checklist rápido para prevenir envios duplicados
Antes de liberar qualquer coisa que possa criar uma cobrança, uma conta, uma mensagem ou um registro, garanta que repetições sejam seguras e previsíveis.
- Nomeie as ações de risco. Anote cada clique que pode criar algo. Se só abre um modal ou muda um filtro, normalmente não precisa de proteção pesada.
- Deixe a UI óbvia. Desabilite imediatamente, mostre um rótulo claro de carregamento e só retorne a clicável quando a ação terminar ou falhar com uma mensagem acionável.
- Faça a API deduplicar repetições. Aceite uma chave de idempotência (ou token similar) em endpoints críticos e retorne o mesmo resultado para a mesma chave.
- Apoie com regras de dados. Use constraints no banco, transações e índices únicos para que duas requisições não escrevam a mesma coisa duas vezes.
- Torne suportável. Logue a chave de idempotência, o resultado final e por que uma duplicata foi bloqueada para que você responda rapidamente a “cobramos duas vezes?”.
Exemplo: impedir um checkout duplicado sem irritar o usuário
Um caso de falha comum: alguém em conexão móvel lenta toca em "Finalizar pedido", nada parece acontecer, então toca de novo. Sem proteção você pode acabar com duas cobranças, dois pedidos e dois e-mails de confirmação.
Um fluxo mais seguro que ainda parece normal:
- No primeiro toque, mude o botão para estado de carregamento e desabilite-o.
- Envie a requisição com uma chave de idempotência (um token único para essa tentativa de checkout).
- Se o usuário tocar de novo, a UI ignora porque o botão está desabilitado.
- Se uma requisição duplicada ainda alcançar o servidor, o servidor retorna o mesmo resultado “pedido criado” em vez de criar outro pedido.
- Mostre um estado de confirmação claro com o número do pedido e um único recibo.
O detalhe-chave é que o servidor faz a proteção final. Controles de UI reduzem repetições acidentais, mas não cobrem todo caso (refreshes, botão voltar, retries após timeout).
Se o usuário voltar mais tarde e tentar de novo, não culpe: mostre algo como “Este pedido já foi feito. Aqui está sua confirmação.” e ofereça um próximo passo simples como “Ver pedido” ou “Entrar em contato com o suporte.”
Próximos passos: proteja uma ação crítica, depois escale o padrão
Escolha uma ação cujo dano seja real se rodar duas vezes: checkout, mudanças de assinatura, envio de fatura, criação de payout ou exclusão de dados. Corrija isso primeiro. Se tentar consertar tudo de uma vez, você vai perder o lugar que realmente importa.
Comece pelo servidor. Adicione uma chave de idempotência (ou equivalente) para que o backend trate requisições repetidas como a mesma operação. Então alinhe a UI com um estado claro de carregamento e mensagens de retry sensatas.
Se seu app foi gerado por uma ferramenta de IA, bugs de envios duplicados costumam estar escondidos em gerenciamento de estado bagunçado: múltiplos handlers de clique, fetchs duplicados, redirects de auth que disparam duas vezes, ou UI otimista que confirma antes do servidor. Nesses casos, um diagnóstico rápido seguido de refatoração direcionada normalmente resolve melhor do que empilhar mais proteções no front-end.
Se quiser uma segunda opinião, FixMyMess (fixmymess.ai) ajuda times a transformar protótipos gerados por IA em software pronto para produção, incluindo diagnóstico de problemas de envios duplicados, reparo de lógica e adição de proteção contra duplicatas no servidor quando faltar.
Perguntas Frequentes
What’s the quickest fix to stop a button from submitting twice?
Desabilite o botão imediatamente e mostre um estado claro de trabalho, como "Salvando...", para que o primeiro clique seja percebido. Ainda assim, adicione idempotência no servidor para qualquer coisa que crie ou finalize algo, porque refreshes e retries podem ignorar a UI.
Which actions actually need double-submit protection?
Qualquer coisa que mude dados ou provoque um efeito externo precisa de proteção: criar registros, cobrar dinheiro, enviar e-mails/SMS, alterar senhas, reservar estoque ou chamar integrações. Leituras simples, como carregar páginas ou buscar, geralmente não exigem proteção pesada.
Is debouncing enough, or should I disable the button?
Debounce só bloqueia toques rápidos dentro de uma janela curta, então não evitará um segundo clique alguns segundos depois em uma rede lenta. Desabilitar com um bloqueio enquanto a requisição está em voo evita repetições durante toda a duração do pedido, que é o que você precisa para envios.
Why do users double-click even when they don’t mean to?
Se a UI não mudar imediatamente, os usuários assumem que o clique não registrou e tentam novamente. Adicione feedback imediato (estado desabilitado, mudança de rótulo, pequena mensagem sobre o tempo estimado) e mantenha o layout estável para que um segundo clique não acerte outro elemento.
Does canceling a request prevent duplicate charges or duplicate creates?
Cancelamento serve principalmente para parar seu app de aplicar uma resposta antiga depois que o usuário seguiu adiante. Não impede duplicatas confiáveis em ações críticas como pagamentos, porque o servidor pode receber e processar ambas as requisições.
What is an idempotency key in plain English?
É um token único por operação pretendida que o cliente envia com a requisição. O servidor armazena o primeiro resultado para essa chave e, se vir a mesma chave novamente, retorna o resultado original em vez de executar o efeito colateral outra vez.
When should I add server-side idempotency, and when is it overkill?
Adote para endpoints que criam ou finalizam algo: checkout, mudanças de assinatura, convites, reset de senha, pagamentos e ações de “criar”. Leituras e ações do tipo “o último comando vence” (buscas, filtros) geralmente não precisam de chaves de idempotência, mas podem se beneficiar de tokens de requisição para evitar atualizações de UI obsoletas.
How do I stop duplicates at the database level?
Adicione uma constraint única para o objeto de negócio “uma vez” (como payment_intent ID ou order attempt ID) para que uma segunda inserção não seja possível. Use transação ou upsert para que duas requisições concorrentes não passem pelo mesmo gate de “verificar então criar”.
What should the UI do when a request is canceled or times out?
Trate como neutro: não mostre um erro assustador e não devolva a UI para o estado “pronto” de forma que encoraje mais cliques. Idealmente, mostre que a ação ainda está em processamento ou confirme o estado final assim que souber o resultado.
My app was generated by an AI tool and it double-submits—what’s usually broken?
Código gerado por IA costuma ter handlers duplicados, múltiplos fetchs disparando para uma ação, ou estado bagunçado que re-aciona envios após redirects e rerenders. Normalmente a solução é rastrear o caminho do clique, adicionar um bloqueio único de pending e colocar idempotência no servidor junto com constraints no banco.