Recommended Free Tools
Um aluno recebeu o dobro dos créditos esperados após comprar um pacote no surfaai: duas linhas completas foram gravadas em user_credits, com segundos de diferença. Segundo o relato de César Eduardo Sturmer, o F5 não duplicou o pagamento; ele podia fazer a página de confirmação chamar novamente a rota que criava os créditos. A correção foi retirar essa responsabilidade do navegador e criar créditos a partir do evento Stripe payment_intent.succeeded, com deduplicação e uma restrição de unicidade no banco.
Como o F5 levou à duplicação
No fluxo anterior, o frontend criava o PaymentIntent, o aluno pagava, a Stripe retornava uma confirmação ao frontend e, então, o frontend chamava uma rota para criar os créditos. A página de confirmação disparava essa chamada ao montar. Recarregá-la podia montá-la de novo; uma oscilação de conexão ou a remontagem do componente também podia repetir a chamada.
O autor relata que a rota validava corretamente o pagamento e o valor. O problema era arquitetural: a criação podia ser invocada repetidamente pelo cliente, sem uma regra que tornasse o efeito válido apenas uma vez. No incidente descrito, isso produziu dois registros completos de crédito, não um segundo pagamento.
Por que o crédito passou a nascer no webhook
Na implementação descrita no postmortem, o único caminho de criação passou a ser o webhook que recebe payment_intent.succeeded. A documentação da Stripe define esse evento como aquele emitido quando um PaymentIntent conclui o pagamento com sucesso (documentação do evento). O frontend deixou de criar créditos e passou a consultar o estado.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →#1 Best Overall
A mudança também resolve uma lacuna prática dos pagamentos que não dependem de o cliente voltar ao site. O autor cita PIX e boleto: alguém pode fechar a aba e pagar mais tarde pelo aplicativo do banco. Se a liberação dependesse da tela de confirmação no navegador, não haveria retorno do cliente para acionar a criação.
A Stripe recomenda um PaymentIntent por pedido ou sessão de cliente e documenta que um PaymentIntent cria no máximo uma cobrança bem-sucedida (documentação de PaymentIntents). Esses comportamentos contextualizam os objetos da Stripe; não explicam, por si sós, os detalhes do código ou do incidente do surfaai.
As três barreiras contra a repetição
1. Usar uma origem server-side
O webhook passou a ser a origem da criação dos créditos, em vez da página de confirmação. Isso impede que um refresh do navegador, sozinho, inicie novamente a operação de criação descrita pelo autor.
Rank #2
2. Registrar e deduplicar o evento
O autor relata que a entrega de webhooks é at-least-once: o mesmo evento pode ser entregue novamente. No início do handler, o código consulta stripe_webhook_events pelo identificador estável stripe_event_id. Se o evento já consta como processado, o handler retorna sucesso sem repetir o trabalho.
3. Deixar a unicidade no banco arbitrar a concorrência
A consulta de existência não basta como garantia final. Duas entregas concorrentes podem consultar antes que qualquer uma grave o registro; ambas passam pela verificação e tentam inserir. No caso relatado, uma restrição de unicidade no banco impede o crédito duplicado. Se a segunda tentativa encontra a violação de unicidade, o código trata o crédito já criado como resultado concluído.
A Stripe também oferece chaves de idempotência para requisições à API: repetir uma requisição de criação ou atualização com a mesma chave pode devolver o resultado registrado, sujeita às regras documentadas de parâmetros e retenção (documentação de chaves de idempotência). Isso é diferente da deduplicação de eventos e da restrição de unicidade implementadas no banco da aplicação; uma não substitui automaticamente as outras.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Repetição não é a mesma coisa que uma condição de corrida
A hipótese inicial do autor foi uma condição de corrida entre chamadas simultâneas, e ele considerou usar um lock. O diagnóstico posterior foi outro: a causa original era a repetição da operação em ocasiões distintas, porque a página podia executá-la de novo sem qualquer regra de uso único. Isso não torna a concorrência irrelevante: no webhook, duas entregas ainda podem passar pela verificação de existência antes da gravação. É por isso que a restrição no banco continua necessária na solução descrita.
O que este caso ensina sobre operações financeiras
- Pergunte o que acontece na segunda execução. Como diz Sturmer no postmortem: “A pergunta útil não era "esse código está certo?", era "o que acontece se isso rodar duas vezes?".”
- Não dependa do retorno ao navegador para confirmar uma compra. Considere como o produto deve agir quando o pagamento é concluído sem que o cliente volte à página.
- Use uma regra persistente de unicidade. Uma consulta na aplicação pode ajudar a evitar trabalho repetido, mas não fecha sozinha a janela entre leitura e gravação concorrentes.
O postmortem foi publicado por César Eduardo Sturmer na DEV Community em 1º de outubro de 2026; a publicação informa que o original saiu no site do autor na mesma data (relato na DEV Community). A causa e os detalhes do diagnóstico aqui descritos são o relato do responsável pelo caso, não uma auditoria independente dos logs, tabelas ou código do surfaai.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesQuick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

