17 de dez. de 2025·8 min de leitura

Corrija condições de corrida: acabe com erros assíncronos intermitentes no seu aplicativo

Aprenda a corrigir condições de corrida identificando comportamentos não determinísticos em filas, requisições web e atualizações de estado, e depois estabilizando-os.

Corrija condições de corrida: acabe com erros assíncronos intermitentes no seu aplicativo

Como são os erros assíncronos intermitentes na prática

Um erro assíncrono intermitente acontece quando você faz a mesma coisa duas vezes e obtém dois resultados diferentes. Você clica no mesmo botão, envia o mesmo formulário ou executa o mesmo job, e o resultado varia. Uma vez funciona. Na outra falha, ou funciona parcialmente e deixa dados em um estado estranho.

O trabalho assíncrono aumenta essa probabilidade porque as tarefas não terminam em linha reta. Requisições, jobs de fila, timers e gravações no banco podem se sobrepor. A ordem pode mudar por pequenas diferenças de tempo: atrasos de rede, uma query lenta no banco, uma retentativa ou um usuário executando a mesma ação duas vezes.

Por isso as condições de corrida costumam parecer aleatórias. O bug é real, mas só aparece quando duas coisas acontecem na ordem infeliz. Por exemplo:

  • Um usuário dá dois cliques em "Pagar agora" e duas requisições criam dois pedidos.
  • Um job em background faz retentativa após timeout, mas a primeira tentativa já havia sido bem-sucedida.
  • Duas abas atualizam o mesmo perfil e a última resposta sobrescreve dados mais recentes.
  • Um webhook chega antes do registro de que ele depende ser confirmado.

Você não precisa ser um especialista para diagnosticar isso. Se conseguir responder o que aconteceu primeiro, já está pensando na direção certa. O objetivo é parar de adivinhar e começar a observar: quais ações foram executadas, em que ordem e o que cada uma acreditava ser o estado atual naquele momento.

Onde as condições de corrida costumam se esconder

Elas raramente estão na linha óbvia de código que você está olhando. Escondem-se nas lacunas entre passos: depois do clique, antes de uma retentativa terminar, enquanto um job de background ainda roda. Se vai consertar condições de corrida, comece mapeando todos os pontos onde o trabalho pode acontecer duas vezes ou em ordem diferente do esperado.

Um esconderijo comum são as filas e jobs em background. Um evento vira dois jobs (ou o mesmo job é reenviado), e ambos funcionam isoladamente. Juntos criam duplicatas, processam fora de ordem ou desencadeiam uma tempestade de retentativas que faz o sistema parecer aleatório.

Requisições web são outra fonte clássica. Usuários clicam duas vezes, redes móveis reenviam, e navegadores mantêm abas paralelas por mais tempo do que você imagina. Duas requisições atingem o mesmo endpoint, cada uma lê o mesmo estado antigo e depois ambas gravam, e a última grava silenciosamente vence.

Atualizações de estado são onde as coisas ficam sutis. Duas atualizações podem ser válidas, mas entram em colisão. Uma atualização posterior pode sobrescrever um valor mais novo porque começou antes, ou porque o código assume que é o único escritor.

Verifique primeiro estes pontos:

  • Consumidores de fila e workers que podem rodar concorrentemente
  • Jobs agendados (cron) que podem se sobrepor quando uma execução fica lenta
  • Lógica de retentativa que não é idempotente
  • Chamadas a APIs de terceiros que podem ter sucesso parcial antes de expirar
  • Qualquer fluxo que faça read-modify-write sem proteção

Exemplo: um app gerado por IA envia um e-mail de boas-vindas tanto a partir de uma requisição web quanto de um job em background "por segurança". Sob carga, ambos os caminhos disparam e você vê duplicatas só às vezes. O problema não é o código do e-mail, é a falta de regra sobre quem pode enviar e quantas vezes.

Sinais rápidos que indicam comportamento não determinístico

Bugs não determinísticos parecem azar: um erro 500 aparece, você atualiza a página e funciona. Esse padrão de "desaparece ao tentar novamente" é um dos sinais mais claros de que lida com timing, não com uma linha de código quebrada.

Observe os dados. Se um registro às vezes some, às vezes está duplicado ou às vezes é sobrescrito por um valor mais antigo, algo está atualizando a mesma coisa de dois lugares. Costuma aparecer como "pagamento capturado duas vezes", "e-mail enviado duas vezes" ou "perfil salvo mas campos revertidos".

Os logs geralmente entregam a história, mesmo quando o bug não aparece. Se vir a mesma ação duas vezes com o mesmo usuário e payload (dois jobs, duas chamadas de API, dois handlers de webhook), assuma concorrência ou retentativas até prova em contrário.

Sinais rápidos para procurar

Esses padrões aparecem repetidamente quando é preciso corrigir condições de corrida:

  • Relatos de bug dizendo "não consegui reproduzir", especialmente em dispositivos ou redes diferentes
  • Erros que disparam quando o tráfego aumenta ou o banco fica mais lento
  • Um fluxo que falha só quando várias abas estão abertas ou usuários clicam duas vezes
  • Uma fila que "às vezes" processa fora de ordem ou gera duplicatas após retentativas
  • Chamados de suporte concentrados em timeouts, retentativas ou desempenho degradado

Exemplo concreto: duas requisições de checkout chegam próximas (duplo clique + resposta lenta). Ambas veem "estoque disponível", ambas reservam e só uma falha depois. A retentativa então "resolve", mascarando o problema real.

Se herdou um protótipo gerado por IA, esses sintomas são comuns porque fluxos assíncronos costumam ser costurados sem dono claro. A FixMyMess audita rapidamente esses caminhos traçando cada requisição e execução de job de ponta a ponta antes de mexer no código.

Instrumente primeiro: o mínimo de logs que vale a pena

Quando tenta consertar condições de corrida adicionando "espera" ou sleeps, normalmente dificulta a detecção do bug. Alguns logs bem escolhidos dirão o que está realmente acontecendo, mesmo quando a falha é rara.

Comece dando a cada ação do usuário ou execução de job um correlation ID. Use-o em todo lugar: na requisição web, no job de fila e em chamadas a serviços downstream. Quando alguém reportar "falhou uma vez", você consegue puxar um fio por todo o sistema em vez de ler ruído não relacionado.

Logue início e fim de cada passo importante, com timestamps. Mantenha o formato simples e consistente, para poder comparar execuções. Também registre o recurso compartilhado em questão, como ID da linha do banco, chave de cache, endereço de e-mail ou nome de arquivo. A maioria dos bugs assíncronos acontece quando dois fluxos tocam a mesma coisa em ordens diferentes.

Um conjunto mínimo de logs que compensa:

  • correlation_id, user_id (ou session_id), e request_id/job_id
  • nome do evento e nome do passo (start, finish)
  • timestamp e duração
  • identificadores de recurso (row ID, cache key, filename)
  • retry_count e error_type (timeout vs validation vs conflict)

Faça logs seguros. Nunca imprima segredos, tokens, senhas em texto claro ou dados completos de cartão. Se precisar confirmar "mesmo valor", registre uma impressão curta (como os últimos 4 caracteres ou uma versão mascarada).

Exemplo: usuário clica em "Pagar" duas vezes. Com correlation IDs e logs de start/finish, você vê duas requisições disputando a atualização do mesmo order_id, uma retentando após timeout. Nesse ponto equipes frequentemente chamam a FixMyMess: auditamos a base e adicionamos instrumentação antes de mexer na lógica, para que a causa real fique óbvia.

Crie uma reprodução confiável sem adivinhar

Bugs assíncronos intermitentes parecem impossíveis porque somem quando você os observa. A forma mais rápida de consertar é parar de "tentar coisas" e construir um caso falho reproduzível.

Escolha um fluxo que falha e escreva a sequência esperada em palavras simples. Mantenha curto, por exemplo: "Usuário clica Pagar -> requisição cria pedido -> job reserva estoque -> UI mostra sucesso." Isso vira sua linha de base do que deveria ocorrer e do que está ocorrendo de fato.

Agora facilite o disparo do bug aumentando pressão no timing e na concorrência. Não espere que ocorra naturalmente.

  • Force ações paralelas: clique duas vezes, abra duas abas ou rode dois workers contra a mesma fila.
  • Adicione delay proposital no passo suspeito (antes de uma gravação, depois de uma leitura, antes de chamar uma API externa).
  • Deixe o ambiente mais lento: limite rede, acrescente latência no banco ou rode a tarefa em loop apertado.
  • Reduza o escopo: reproduza no menor handler ou job que conseguir isolar.
  • Anote entradas e a configuração de timing exata para que qualquer um possa repetir.

Exemplo concreto: um botão "Criar Projeto" às vezes cria dois projetos. Coloque um delay de 300ms imediatamente antes do insert, depois clique duas vezes rápido ou submeta a partir de duas abas. Se conseguir disparar duplicatas 8 em 10 vezes, tem algo concreto para trabalhar.

Mantenha um roteiro curto de reprodução (mesmo que sejam poucos passos manuais) e trate-o como um teste: rode antes e depois de cada mudança. Se herdou um app gerado por IA, times como a FixMyMess costumam começar construindo esse repro primeiro, porque transforma uma queixa vaga em uma falha mensurável.

Passo a passo: estabilize o fluxo em vez de perseguir o timing

Se tentar consertar condições de corrida adicionando delays, o bug geralmente migra. O objetivo é tornar o fluxo seguro mesmo quando ações ocorrem duas vezes, fora de ordem ou ao mesmo tempo.

Comece nomeando a coisa compartilhada que pode ser corrompida. Muitas vezes é uma única linha (registro de usuário), um contador (saldo) ou um recurso limitado (estoque). Se dois caminhos podem tocar isso ao mesmo tempo, esse é o problema real, não o timing.

Uma forma prática de consertar é escolher uma estratégia de segurança e aplicá-la consistentemente:

  1. Mapeie quem lê e quem escreve o recurso compartilhado (requisição A, job B, webhook C).
  2. Decida a regra: serializar o trabalho (um por vez), travar o recurso, ou tornar a operação segura para repetir.
  3. Adicione uma chave de idempotência para qualquer ação que possa ser reenviada (duplo clique, retry de rede, reentrega da fila).
  4. Proteja gravações com transação ou atualização condicional para não perder a mudança de outro.
  5. Comprove com testes de concorrência e execuções repetidas, não com um único "parece ok na minha máquina".

Exemplo: duas requisições "Fazer pedido" chegam ao mesmo tempo. Sem proteção, ambas leem inventory=1, subtraem e você envia dois itens. Com idempotência, a segunda requisição reutiliza o resultado da primeira. Com uma atualização condicional (só subtrair se inventory ainda for 1) dentro de uma transação, apenas uma vence.

Se herdou código gerado por IA, essas salvaguardas frequentemente faltam ou são inconsistentes. A FixMyMess normalmente começa adicionando o mínimo de idempotência e regras de gravação segura, depois repete o mesmo cenário dezenas de vezes até ficar estável.

Filas e jobs em background: duplicatas, retentativas, ordenação

A maioria das filas garante entrega pelo menos uma vez. Isso significa que o mesmo job pode rodar duas vezes ou rodar depois de um job mais novo, mesmo se você só clicou uma vez. Se o handler assume que é a única execução, surgem resultados estranhos: cobranças duplicadas, e-mails em duplicidade ou um registro alternando entre estados.

A abordagem mais segura é tornar cada job seguro para repetir. Pense em resultados, não em tentativas. Um job deve poder rodar novamente e terminar no mesmo estado final.

Um padrão prático que ajuda em processamento background:

  • Adicione uma chave de idempotência (orderId, userId + action + date) e marque "já processado" antes de fazer efeitos colaterais.
  • Registre um status claro do job (pending, running, done, failed) para que re-runs possam sair cedo.
  • Trate chamadas externas (pagamento, e-mail, upload) como passos "fazer uma vez" com checagens de deduplicação próprias.
  • Proteja contra eventos fora de ordem usando número de versão ou timestamp e ignore atualizações antigas.
  • Separe falhas retryáveis (timeouts, limites de taxa) das não retryáveis (input inválido) e pare de retentar quando não adiantar.

Problemas de ordenação são fáceis de perder. Exemplo: um job "cancelar assinatura" roda e depois chega um job atrasado "renovar assinatura" que reativa o usuário. Se armazenar uma versão monotonicamente crescente (ou updatedAt) a cada mudança, o handler pode rejeitar mensagens obsoletas e manter a verdade mais recente.

Tenha cuidado com locks globais. Eles podem esconder o bug desacelerando tudo e depois prejudicar a produção bloqueando trabalho não relacionado. Prefira bloqueios por entidade (um usuário ou um pedido por vez) ou checagens de idempotência.

Se herdou um worker gerado por IA que duplica trabalho aleatoriamente, equipes como a FixMyMess costumam começar adicionando idempotência e checagens de "evento obsoleto". Essas duas mudanças geralmente transformam comportamento intermitente em resultados previsíveis rapidamente.

Requisições web: duplo clique, retentativas e sessões paralelas

A maioria dos bugs de requisição ocorre quando a mesma ação é enviada duas vezes ou quando duas ações chegam em ordem "errada". Usuários clicam duas vezes, navegadores reenviam em rede lenta, apps móveis reencaminham após timeout e várias abas agem como pessoas distintas.

Para consertar aqui, assuma que o cliente pode e vai enviar duplicatas. O servidor precisa estar correto mesmo que a UI esteja errada, lenta ou offline por um momento.

Torne endpoints de "fazer a ação" seguros para repetir

Se uma requisição pode criar efeitos colaterais (cobrar cartão, criar pedido, enviar e-mail), torne-a idempotente. Uma abordagem simples é exigir uma chave de idempotência por ação. Armazene-a com o resultado e, se a mesma chave reaparecer, retorne o mesmo resultado.

Também fique atento a timeouts. Uma falha comum: o servidor ainda está processando, o cliente dá timeout e tenta novamente. Sem deduplicação, você tem dois pedidos, dois e-mails de reset ou duas mensagens de boas-vindas.

Conjunto rápido de proteções que costuma valer a pena:

  • Exigir chave de idempotência para endpoints de create/submit e deduplicar por (usuário, chave)
  • Usar respostas de erro consistentes para que clientes só reenviem em erros seguros
  • Logar request ID e idempotency key em cada tentativa
  • Fazer efeitos colaterais só depois de persistir o registro "essa ação aconteceu"
  • Tratar "já feito" como sucesso, não como erro assustador

Evite que requisições paralelas sobrescrevam estado

Sobrescritas ocorrem quando duas requisições leem o mesmo estado antigo e depois escrevem atualizações. Exemplo: duas abas atualizam um perfil e a última vence, apagando silenciosamente a outra.

Prefira checagens no servidor como números de versão (optimistic locking) ou regras explícitas como "só atualizar se status ainda for PENDING". Se herdou handlers gerados por IA que fazem read-modify-write sem proteção, aqui é um lugar comum onde a FixMyMess vê relatos aleatórios de "às vezes salva, às vezes não".

Atualizações de estado: evitando sobrescritas e dados inconsistentes

Muito do comportamento intermitente não é "assíncrono" em filas ou rede. São duas partes do app atualizando a mesma peça de estado em ordens diferentes. Hoje vence uma atualização, amanhã vence outra.

O problema clássico é lost update: dois workers leem o mesmo valor antigo, cada um calcula um novo valor e a última gravação sobrescreve a primeira. Exemplo: dois dispositivos atualizam as configurações de notificação de um usuário. Ambos leem "habilitado", um desativa, outro muda o som, e o registro final perde uma mudança.

Quando possível, prefira operações atômicas em vez de ler e depois escrever. Bancos oferecem primitivas seguras como incremento, compare-and-set e "update where version = X". Isso transforma timing em regra clara: só uma atualização pode vencer e o perdedor re-tenta com dados novos.

Outra solução é validar transições de estado. Se um pedido só pode ir de pending -> paid -> shipped, rejeite shipped -> paid mesmo que chegue atrasado. Isso impede que requisições tardias, retentativas ou jobs revertam estado.

Cache pode piorar. Uma leitura obsoleta do cache pode levar a uma escrita "correta" baseada em dados antigos. Se você cacheia estado que direciona gravações, ou invalide o cache nas atualizações ou leia da fonte da verdade imediatamente antes de escrever.

Uma forma simples de consertar é propriedade: decida um lugar que tenha permissão para atualizar uma peça de estado e faça todas as mudanças por ele. Regras de propriedade boas costumam ser:

  • Um serviço é dono da escrita, outros apenas requisitam mudanças
  • Uma tabela ou documento é a fonte da verdade
  • Atualizações incluem número de versão (ou timestamp) e são rejeitadas se estiverem obsoletas
  • Só são aceitas transições de estado permitidas

Na FixMyMess, frequentemente vemos apps gerados por IA atualizando o mesmo registro a partir do código da UI, handlers de API e jobs de background ao mesmo tempo. Tornar a propriedade explícita costuma ser a forma mais rápida de evitar dados inconsistentes.

Armadilhas comuns que mantêm bugs intermitentes vivos

A forma mais rápida de perder uma semana com esses bugs é "tratar o relógio" em vez da causa. Se o bug depende de timing, você pode fazê-lo desaparecer por um dia e ainda assim entregar o problema.

Um erro comum é aumentar retentativas quando não há idempotência. Se um job de pagamento falha no meio e depois re-tenta, pode cobrar duas vezes. Retentativas só são seguras quando cada tentativa pode rodar novamente sem alterar o resultado.

Outra armadilha é espalhar delays aleatórios para "afastar colisões". Isso costuma esconder o problema em staging e depois piorar em produção porque padrões de carga mudam. Delays também deixam o sistema mais lento e difícil de entender.

Grandes locks podem sair pela culatra. Um mutex gigante em todo o fluxo pode parar o flake, mas causar novos modos de falha: esperas longas, timeouts e retentativas em cascata que trazem o bug de volta em outra forma.

Fique de olho nesses padrões que mantêm comportamento não determinístico:

  • Tratar toda falha como retryável (timeouts, validação, auth e conflitos exigem respostas diferentes)
  • Declarar vitória porque passou localmente uma vez (concorrência real raramente aparece em um laptop silencioso)
  • Logar só "erro aconteceu" sem request/job id, número de tentativa ou versão do estado
  • Consertar sintomas em uma camada enquanto a corrida ainda existe por baixo (UI, API e worker podem se sobrepor)
  • Gambiarras temporárias que viram permanentes (retentativas extras, sleeps ou blocos catch-all)

Se herdou uma base gerada por IA com esses curativos, uma auditoria focada rapidamente mostra onde retentativas, locks e falta de idempotência estão mascarando a corrida real. Aí uma reparação dirigida supera mais tentativa e erro.

Checklist rápido antes de enviar a correção

Antes de dar por concluído, garanta que não está apenas "vencendo a loteria do timing". O objetivo é consertar condições de corrida para que as mesmas entradas produzam sempre os mesmos resultados.

Checklist pré-ship que pega a maioria dos culpados:

  • Execute duas vezes de propósito. Dispare a mesma ação duas vezes (duplo clique, dois workers, duas abas) e confirme que o resultado é correto, não só "não quebrou".
  • Encontre a coisa compartilhada. Identifique o recurso compartilhado (row, arquivo, chave de cache, saldo, inbox) e decida como está protegido: transação, lock, constraint único ou atualização condicional.
  • Audite retentativas para efeitos colaterais. Se uma retentativa envia outro e-mail, cobra de novo ou grava uma linha duplicada, adicione idempotência para que "mesma requisição" signifique "mesmo efeito".
  • Compare a ordenação nos logs. Em uma execução boa vs ruim, os eventos chegam em ordem diferente? Diferenças de ordenação indicam que você pode ter consertado sintomas, não a causa.
  • Prefira garantias atômicas a sleeps. Se uma transação, índice único ou "update só se versão casar" remove o bug, isso é mais seguro que acrescentar delays.

Exemplo: se "Criar assinatura" às vezes cobra duas vezes, verifique que a chamada ao provedor de pagamento usa token de idempotência e que a gravação no banco tem constraint único nesse token. Assim duplicatas viram no-op em vez de prejuízo para o cliente.

Exemplo: estabilizando um fluxo intermitente de ponta a ponta

Imagine um fluxo simples: dois colegas editam o mesmo cadastro de cliente e um job em background também atualiza esse registro após um import. Em demos tudo parece ok, mas com usuários reais aparecem resultados estranhos.

Hoje, o app usa last write wins. Usuário A salva, depois B salva e sobrescreve A sem aviso. Ao mesmo tempo, o job da fila re-tenta após timeout e envia a notificação "Cliente atualizado" duas vezes.

Para confirmar que é não determinístico (e para consertar em vez de adivinhar), crie um repro repetível:

  • Abra o registro em duas abas (A e B)
  • Altere campos diferentes em cada aba
  • Clique em salvar em ambas as abas dentro de um segundo
  • Dispare o job de background e force uma retentativa (por exemplo jogando um erro temporário após o envio)
  • Verifique o estado final do registro e a quantidade de notificações

Se cada execução terminar diferente, encontrou o bug de timing.

Estabilizar geralmente exige duas mudanças. Primeiro, torne notificações idempotentes: adicione uma chave tipo customerId + eventType + version e armazene, para que a mesma notificação não seja enviada duas vezes mesmo com retentativas.

Segundo, torne a atualização do registro atômica. Envolva a atualização em uma transação e adicione checagem de versão (optimistic locking). Se a aba B tentar salvar uma versão antiga, retorne uma mensagem clara pedindo para atualizar e tentar de novo, em vez de sobrescrever silenciosamente.

Reteste o mesmo repro 50 vezes. Você quer resultados idênticos a cada execução: um estado final explicável e exatamente uma notificação.

Esse tipo de problema é comum em apps gerados por IA: retentativas e código assíncrono existem, mas as proteções (idempotência, locking, transações) faltam.

Próximos passos: torne o sistema previsível de novo

Escolha os fluxos onde a instabilidade mais te afeta. Não comece por "o app todo". Foque nos 2–3 fluxos críticos onde um resultado ruim custa caro, como: cobrar um cartão, criar uma conta, fazer um pedido, enviar e-mail ou atualizar estoque.

Escreva esses fluxos em passos simples (o que dispara, o que é chamado, que dados mudam). Esse pequeno mapa dá uma verdade compartilhada quando houver discussão sobre ordering e facilita consertar sem adivinhar.

Escolha uma salvaguarda que você consiga enviar esta semana. Mudanças pequenas frequentemente eliminam a maior parte do risco:

  • Adicionar chave de idempotência na ação que cria algo (pagamentos, pedidos, e-mails)
  • Usar atualização condicional (só atualizar se a versão casar ou se o status estiver como esperado)
  • Adicionar constraint único para que duplicatas falhem rápido e com segurança
  • Enforçar ordenação para um tópico de fila (ou colapsar vários jobs em um job de "estado mais recente")
  • Colocar timeout e limite de retentativas onde hoje retentativas rodam para sempre

Se sua base foi gerada por ferramentas de IA e está difícil de entender, planeje uma limpeza focada: um fluxo, um responsável e uma semana para remover estado compartilhado oculto e retentativas mágicas.

Exemplo: se "Criar pedido" às vezes envia dois e-mails de confirmação, torne o envio do e-mail idempotente primeiro, depois ajuste o worker da fila para que ele possa retentar com segurança sem alterar o resultado.

Se quiser um plano rápido e claro, a FixMyMess pode rodar uma auditoria de código gratuita para apontar condições de corrida, retentativas e gravações inseguras. E se precisar estabilizar rápido, podemos diagnosticar e reparar protótipos gerados por IA e prepará-los para produção em 48–72 horas com verificação especialista.