Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.