O que é uma race condition
O resultado depende da ordem de execução, que não é garantida
Uma race condition ocorre quando o resultado correto de uma operação depende da ordem ou do timing de execução de processos concorrentes — e essa ordem não é garantida. O nome é literal: dois processos "correndo" para acessar ou modificar o mesmo recurso, e o resultado depende de qual chega primeiro. Em um ambiente de execução único e sequencial, race conditions não existem. Em produção, com múltiplos threads, múltiplas instâncias de serviço, workers concorrentes e operações assíncronas, race conditions são uma das fontes mais comuns de bugs que só aparecem sob carga ou em condições específicas de timing.
O exemplo clássico — decrement de estoque
Dois processos decrementam o mesmo contador e chegam a resultado impossível
Dois workers verificam simultâneamente o estoque de um produto: ambos leem stock = 1. Ambos verificam que 1 maior que 0, então é possível vender. Ambos decrementam: stock = stock - 1. Resultado: stock = -1. Um produto com estoque negativo foi vendido para dois clientes. O problema é que a sequência "ler, verificar, decrementar" não é atômica — entre a leitura e a escrita, outro processo pode modificar o mesmo registro. A solução é tornar a operação atômica: UPDATE products SET stock = stock - 1 WHERE id = $1 AND stock > 0, verificando as linhas afetadas. Zero linhas afetadas = sem estoque, falhar gracefully.
Check-then-act — o antipadrão que gera race conditions
Verificar uma condição e agir baseado nela sem garantir que a condição ainda vale
O padrão check-then-act é a principal fonte de race conditions: ler um valor, tomar uma decisão baseada nele, e então agir — sem garantir que o valor não mudou entre a leitura e a ação. Exemplos: verificar se um nome de usuário está disponível e então criá-lo (outro processo pode criar o mesmo nome no meio), verificar se um arquivo existe antes de criá-lo (dois processos criam o arquivo ao mesmo tempo), verificar o saldo antes de debitar (dois débitos simultâneos podem usar o mesmo saldo). A solução é substituir check-then-act por operações condicionais atômicas: INSERT ... ON CONFLICT DO NOTHING, UPDATE ... WHERE condição, ou operações que verificam e agem em uma única instrução indivisível.
Race conditions em código assíncrono
Async não significa automaticamente livre de race conditions
Código assíncrono com await é sequencial dentro de uma única execução, mas múltiplas execuções simultâneas podem criar race conditions. Em Node.js, por exemplo: dois requests chegam ao mesmo handler, ambos executam await getUserBalance(), ambos veem saldo 100, ambos executam await debitUser(100), resultado: saldo -100. O fato de o código usar await não protege contra concorrência entre requisições simultâneas. A proteção precisa estar na camada de banco de dados (operação atômica), em locks distribuídos, ou em design que evita o estado compartilhado mutable.
Deadlocks — quando dois processos esperam um pelo outro
Um ciclo de dependência que paralisa os dois processos indefinidamente
Deadlock é um caso especial de race condition onde dois processos cada um segura um recurso e espera pelo recurso que o outro processo segura — ciclo de dependência que nunca resolve. Processo A segura lock da tabela de usuários e espera o lock da tabela de pedidos. Processo B segura lock da tabela de pedidos e espera o lock da tabela de usuários. Nenhum pode avançar. Bancos de dados detectam deadlocks e matam uma das transações (a "vítima"), que recebe deadlock error e precisa ser refeita. A prevenção é consistência na ordem de aquisição de locks: sempre adquirir locks na mesma ordem (usuários antes de pedidos, não o contrário).
Optimistic locking — detectar conflito sem bloquear
Versioning para detectar que alguém modificou o dado entre a leitura e a escrita
Optimistic locking resolve race conditions sem bloquear recursos: cada registro tem um campo de versão (ou timestamp de atualização). Ao ler um registro, capturar a versão. Ao atualizar, incluir a versão como condição: UPDATE ... WHERE id = $1 AND version = $versao_lida. Se zero linhas foram afetadas, significa que outra operação modificou o registro — o conflito é detectado e a operação pode ser refeita com os dados atualizados. É mais performático que pessimistic locking (sem bloqueio) e correto quando conflitos são raros. ORMs como Hibernate, Entity Framework e ActiveRecord têm suporte nativo a optimistic locking.
Distributed locks — coordenação entre serviços
Como garantir que apenas uma instância execute uma operação crítica
Quando múltiplas instâncias de um serviço ou múltiplos microsserviços precisam coordenar acesso exclusivo a um recurso compartilhado, distributed lock via Redis é o padrão mais comum. O algoritmo usa SET key value NX PX ttl — set atômico se não existir, com expiração automática. A instância que conseguir criar a chave tem o lock; as outras tentam com backoff. A expiração automática garante que o lock seja liberado mesmo se a instância que o adquiriu travar. Cuidado: locks distribuídos não são perfeitos — network partitions e falhas de relógio podem criar situações onde dois processos acreditam ter o lock simultaneamente (o Redlock paper do antirez discute os trade-offs).
Design para evitar estado compartilhado mutable
A melhor proteção contra race conditions é não ter o problema em primeiro lugar
A abordagem mais elegante é design que evita estado compartilhado mutable: cada operação trabalha com dados que são de sua exclusividade, sem competir com outros processos. Event sourcing e CQRS naturalmente reduzem race conditions ao tornar escrita um append de eventos (imutável) em vez de update de estado. Actor model (como Erlang/OTP ou Akka) isola estado por actor, eliminando compartilhamento direto. Em microsserviços, cada serviço deve ser o único dono de seus dados — se dois serviços modificam o mesmo registro, o design está errado. Comunicação via eventos em vez de banco compartilhado elimina a raiz do problema.
Testing — como reproduzir e verificar race conditions
Race conditions raramente aparecem em testes sequenciais
Testar race conditions é desafiador porque dependem de timing específico. Abordagens úteis: testes de stress com concorrência controlada (disparar 100 requests simultâneos para o mesmo endpoint e verificar que o resultado é consistente), testes com sleep artificiais para ampliar a janela de race condition (injecting sleeps between check and act), fuzzing de concorrência com ferramentas como Go race detector, e testes de integração com banco real em vez de mocks (mocks raramente capturam comportamento de locking). O banco de dados é o árbitro final — sempre testar com banco real para operações concorrentes críticas.
Conclusão — concorrência correta é design deliberado
Race conditions não são bugs de azar — são consequências de design sem controle de concorrência
Race conditions não aparecem por acidente de implementação — aparecem quando o design não considera concorrência como um requisito. Em qualquer sistema com múltiplas execuções simultâneas (que é todo sistema web em produção), operações que compartilham estado mutable devem ser explicitamente protegidas com locks, operações atômicas, optimistic locking ou design que evite o compartilhamento. Sistemas com alta concorrência que não consideram race conditions desde o design terão bugs difíceis de reproduzir e custos altos para corrigir depois. Continue em: Fundamentos obrigatórios antes de produção.
Race Conditions e Concorrência — Vídeos
What is a Race Condition? — Nutshell
Race Conditions: The Weirdest Bug — Core Dumped
Idempotency — base para operações concorrentes seguras
What is API Idempotency — concorrência e atomicidade
How does the Internet work — concorrência em rede
Master Software Architecture — design para concorrência
Conceitos-chave
Race Condition
Bug onde o resultado depende da ordem de execução de processos concorrentes — resultado imprevisível.
Check-then-Act
Antipadrão: verificar condição e agir sem garantir que a condição ainda vale — janela para race condition.
Optimistic Locking
Estratégia com versioning que detecta conflito no momento do update sem bloquear o registro.
Pessimistic Locking
Bloqueia o registro para outros processos durante a operação (SELECT FOR UPDATE).
Deadlock
Situação onde dois processos cada um espera pelo lock do outro — ciclo que nunca resolve.
Distributed Lock
Lock coordenado entre múltiplas instâncias de serviço — tipicamente implementado com Redis SETNX.
Sistemas Distribuídos no Instagram
@bytebytego
Reels — Arquitetura e Backend
@bytebytego
ByteByteGo no Facebook
Sistemas em Produção no X
Links Úteis
O que dizem
O exemplo do estoque negativo era exatamente o bug que tínhamos. Dois workers de processamento de pedido lendo e decrementando o mesmo produto. O UPDATE com WHERE stock > 0 e verificação de rows affected resolveu completamente. Simples e eficaz.
O ponto sobre async/await e race conditions é fundamental. Muita gente acha que Node.js é single-threaded então não tem race condition — mas múltiplas requisições simultâneas criam exatamente o mesmo problema. A proteção precisa estar no banco, não no runtime.
Ótimo artigo. Vale mencionar que o Go tem um race condition detector nativo (go test -race) que identifica race conditions em tempo de teste — é muito útil para detectar problemas antes de ir para produção. Outras linguagens têm ferramentas similares.