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

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

@mjovanovictech

Como testar resiliência de sistemas em produção real

Ver post completo no X →
@mjovanovictech

Padrões de resiliência em .NET Core com exemplos

Ver post completo no X →
@mjovanovictech

Arquitetura de software orientada a domínio

Ver post completo no X →
@mjovanovictech

Lições de 5 anos mantendo sistemas em produção

Ver post completo no X →
@mjovanovictech

Design de APIs resilientes para produção

Ver post completo no X →
@mjovanovictech

Microsserviços vs monolito — como escolher

Ver post completo no X →

O que dizem

Igor S. ★★★★★

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.

Marina T. ★★★★★

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.

Roberto L. ★★★★☆

Ó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.