Conteúdo

BCrypt é o algoritmo certo para armazenar senhas, com salt embutido e resistente a força bruta. Mas ele tem uma característica que quase ninguém dimensiona: é deliberadamente caro em CPU, e o custo real depende do hardware onde roda. Este artigo parte de um caso real, uma API ASP.NET Core em um Azure App Service Basic B2 onde um único reset de senha levava 2,8 segundos, dos quais 2,4 segundos eram o BCrypt queimando um núcleo a 100%. Com work factor 12 calibrado para uma máquina forte e apenas 2 vCPUs disponíveis, bastavam poucos logins simultâneos para saturar a CPU, disparar thread pool starvation e derrubar a aplicação inteira — uma amplificação de DoS com custo baixíssimo para o atacante.

Além do diagnóstico com Application Insights, o artigo traz um benchmark real com BenchmarkDotNet comparando o BCrypt.Net-Next em vários work factors contra o PasswordHasher<T> do ASP.NET Core Identity (PBKDF2 com HMAC-SHA512 e 100 mil iterações), mostra como calibrar o work factor para o seu hardware seguindo a recomendação da OWASP, como limitar a concorrência de hashing com SemaphoreSlim, como re-hashear senhas antigas no próximo login com PasswordNeedsRehash — e fecha com uma análise de custo em 12 meses: resolver com código sai de graça; resolver com infraestrutura custa para sempre.

Insights

  1. O BCrypt é lento de propósito, mas o quanto ele é lento depende do seu hardware: o work factor é um expoente — 2^12 = 4096 rodadas. Um hash que leva ~250 ms numa máquina de desenvolvimento pode levar 2,3 segundos num Azure App Service Basic B2. O número foi copiado de uma recomendação genérica; o hardware, não.
  2. Verify custa exatamente o mesmo que HashPassword no mesmo fator: para comparar a senha, o BCrypt re-executa o KDF inteiro com o salt embutido no hash. Cada login paga o preço cheio — e login é a operação de autenticação mais frequente do sistema, não o reset de senha.
  3. Hash de senha é CPU-bound puro e não existe await que resolva: um hash BCrypt ocupa um núcleo inteiro a 100% do início ao fim. Em uma máquina de 2 núcleos, 3 logins simultâneos já saturam o processo inteiro — carrinho, busca e health check degradam junto.
  4. A recomendação da OWASP não é um número mágico, é uma calibração: work factor mínimo de 10 e “tão alto quanto a performance do servidor de verificação permitir”. A medida certa é o tempo por hash no hardware de produção — não o valor que funcionou no blog de outra pessoa.
  5. Corrigir no código custa uma linha; corrigir na infraestrutura custa todos os meses: baixar o work factor de 12 para 10 corta o custo de CPU por autenticação em 4x e sai de graça. Escalar de B2 para P1v3 no Azure custa mais de US$ 1.500 adicionais por ano — e não elimina a causa raiz.

O que o BCrypt faz e por que ele é lento de propósito

BCrypt é uma função de derivação de chave (KDF) adaptativa criada em 1999 por Niels Provos e David Mazières, baseada no algoritmo Blowfish e projetada para ser cara de calcular — de propósito, com custo ajustável por configuração.

Diferente de um hash de uso geral como SHA-256, que processa milhões de operações por segundo, o BCrypt impõe um preço a cada tentativa. A lógica é simples: se o banco de hashes vazar, cada palpite de força bruta do atacante paga o mesmo custo computacional que o seu servidor paga para validar um login legítimo.

Work factor (fator de trabalho) é o parâmetro do BCrypt que define esse custo: o número de rodadas do algoritmo é 2 elevado ao work factor — fator 12 significa 4.096 rodadas. Cada incremento de 1 dobra o tempo de CPU necessário para calcular ou verificar um hash.

E aqui mora o primeiro detalhe que muita gente ignora: o work factor é um expoente, não um multiplicador. O crescimento das rodadas do key setup do algoritmo EksBlowfish (expensive key schedule Blowfish) é geométrico:

Work factorRodadasCusto relativo
101.0241x
112.0482x
124.0964x
138.1928x
1416.38416x

Cada incremento de 1 no work factor dobra o tempo de CPU. O fator 12 não é “um pouco mais seguro” que o 10 — é exatamente 4 vezes mais caro, para o atacante e para o seu servidor.

No .NET, a implementação de referência é o pacote BCrypt.Net-Next (versão 4.2.0, com target para .NET 10), uma implementação 100% managed portada do jBCrypt. O uso típico:

O hash resultante carrega tudo que o algoritmo precisa para validar a senha depois — versão, work factor e salt — embutidos na própria string:

Guarde esse detalhe: o work factor viaja dentro do hash. Ele será decisivo na hora de corrigir o problema sem quebrar as senhas existentes.

Há mais duas propriedades que definem o comportamento do BCrypt em produção:

  • É single-threaded: um hash não paraleliza. Ele usa um único núcleo, do início ao fim.
  • É 100% CPU-bound: não há I/O, não há espera, não há pausa. O núcleo fica a 100% durante todo o cálculo. Não existe await que devolva essa CPU — assíncrono resolve espera, não processamento.

O diagrama abaixo resume o ciclo de CPU de um hash BCrypt no .NET — todas as rodadas em um único núcleo, sem espera:

flowchart LR
    A[Senha em texto] --> B[EksBlowfish key setup]
    B --> C{"2^WF rodadas<br/>(WF=12 → 4.096)"}
    C -->|"CPU 100%<br/>um núcleo inteiro"| C
    C --> D[Hash final com salt e WF embutidos]

O caso real de uma API que travava com um reset de senha

O cenário: uma API de e-commerce em ASP.NET Core rodando em um Azure App Service plano Basic B2 — 2 vCPUs e 3,5 GB de RAM. O serviço de senha era enxuto e aparentemente correto:

O sintoma apareceu no Application Insights, em um request de reset de senha:

  • Latência total do POST /Auth/ResetPassword: ~2.804 ms
  • Tempo em SQL: ~32 ms
  • Tempo dentro do BCrypt: ~2.377 ms
  • CPU do plano: 100% durante toda a operação

Um único hash custava ~2,3 segundos naquele hardware. Praticamente todo o tempo de resposta era o BCrypt queimando um núcleo. O banco de dados — o suspeito de sempre — respondia em 32 ms e era inocente.

E o reset de senha era só a ponta visível. Mapeando todos os fluxos que passavam pelo PasswordService:

FluxoOperação BCryptCusto no B2
LoginVerify~2,3 s por login
Login de usuário legado (primeira vez)Verify legado + HashPassword (migração)~4,6 s
Cadastro de clienteHashPassword~2,3 s
Reset de senhaHashPassword~2,3 s
Troca de senhaVerify + HashPassword~4,6 s

Antes de apontar os erros, vale registrar o que estava certo nesse código — porque a base era boa:

  • Usar BCrypt é a decisão correta. Hash adaptativo com salt, nada de MD5, SHA puro ou senha em texto.
  • O work factor era configurável e havia migração de senhas legadas (texto plano → BCrypt no primeiro login) — um padrão excelente.
  • Havia rate limiting por IP, protegendo contra um atacante único martelando o endpoint de login.

O problema não era o algoritmo. Era a calibração para o hardware errado — e o que acontece quando ninguém dimensiona a concorrência.

Verify custa o mesmo que HashPassword

Este é o detalhe que quase todo mundo esquece, e ele muda completamente a análise de impacto.

Para validar uma senha, o BCrypt não “compara hashes”. Ele extrai o salt e o work factor do hash armazenado, re-executa o KDF inteiro sobre a senha informada e só então compara o resultado. A sequência de um login com BCrypt no Basic B2 fica assim:

sequenceDiagram
    participant U as Usuário
    participant API as LoginHandler
    participant BC as BCrypt.Verify

    U->>API: POST /Auth/Login (email, senha)
    API->>BC: Verify(senha, hashArmazenado)
    Note over BC: Extrai salt + WF do hash<br/>Re-executa 2^12 rodadas<br/>CPU 100% por ~2,3 s (B2)
    BC-->>API: true / false
    API-->>U: 200 OK (após ~2,3 s)

A consequência prática: não é o reset de senha (raro) que define o custo do sistema — é o login (frequente). Cada autenticação bem-sucedida ou falha paga os mesmos 2^work factor. Um endpoint de login com BCrypt em work factor 12 num B2 é, na prática, um endpoint que consome 2,3 segundos de um núcleo por chamada.

A matemática do colapso em dois núcleos

Com os números do caso, o colapso deixa de ser mistério e vira aritmética:

  • 1 hash = 1 núcleo a 100% por ~2,3 s
  • O plano B2 tem 2 núcleos
  • A partir de 2–3 autenticações simultâneas, todos os núcleos estão saturados

E aqui entra o efeito que transforma um problema de autenticação em um problema do processo inteiro: o hash roda no mesmo thread pool que serve todos os requests da aplicação. Com a CPU em 100%, tudo desacelera junto — carrinho, busca, catálogo e, criticamente, o health check. A cascata do colapso causado pelo BCrypt em dois núcleos:

flowchart TD
    A[5 logins simultâneos<br/>de IPs diferentes] --> B[5 hashes BCrypt<br/>disputando 2 núcleos]
    B --> C[CPU 100% sustentada]
    C --> D[Threads do pool ocupadas<br/>com trabalho CPU-bound]
    D --> E[Requests de carrinho, busca<br/>e checkout enfileiram]
    E --> F[Timeouts em cascata]
    C --> G[Health probe do App Service<br/>não responde a tempo]
    G --> H[App Service reinicia o container]
    H --> I[Indisponibilidade total<br/>para todos os usuários]

Os agravantes:

  1. Zero controle de concorrência de CPU. N logins simultâneos = N hashes disputando 2 núcleos. Não havia fila, não havia limite, não havia fast-fail.
  2. O rate limiter por IP não cobre concorrência orgânica. Ele barra 1 IP abusando — não barra 5 clientes legítimos, de IPs diferentes, logando ao mesmo tempo. Tráfego normal de pico já era suficiente para o colapso.
  3. A fome de CPU contamina a API inteira. Escrevi em detalhe sobre esse mecanismo no artigo sobre thread pool starvation no .NET — quando a CPU satura com trabalho CPU-bound, o thread pool não consegue atender nem o heartbeat do Kestrel.

O resultado é uma amplificação de DoS: um punhado de tentativas de login — nem precisam ser maliciosas — derruba a API inteira. Custo do atacante: baixíssimo. Dano: total. É exatamente o trade-off que a OWASP descreve no Password Storage Cheat Sheet ao recomendar que o work factor seja “tão alto quanto a performance do servidor de verificação permitir” — permitir é a palavra-chave; o servidor de produção é o limite, não o desejo de segurança.

Benchmark do BCrypt contra o PasswordHasher do ASP.NET Core

Para tirar a conversa do achismo, rodei um benchmark com BenchmarkDotNet comparando o BCrypt.Net-Next 4.2.0 em vários work factors contra o PasswordHasher<T> do ASP.NET Core Identity — que, desde o .NET 7, usa por padrão o formato V3: PBKDF2 com HMAC-SHA512, 100.000 iterações, salt de 128 bits e subchave de 256 bits.

Resultado na minha máquina de desenvolvimento (Intel Core i7-8665U, 4 núcleos físicos, .NET 10.0.1, build Release):

MétodoMédiaDesvio padrãoAlocação
BCrypt_Hash_WF1066,58 ms1,338 ms5.448 B
BCrypt_Hash_WF11130,71 ms2,669 ms5.460 B
BCrypt_Hash_WF12269,62 ms4,944 ms5.448 B
BCrypt_Hash_WF13523,81 ms7,803 ms5.448 B
BCrypt_Hash_WF141.035,31 ms7,648 ms5.448 B
BCrypt_Verify_WF1066,46 ms1,757 ms5.264 B
BCrypt_Verify_WF12258,16 ms7,043 ms5.264 B
Identity_PBKDF2_Hash63,60 ms3,653 ms436 B
Identity_PBKDF2_Verify62,67 ms2,761 ms297 B

Quatro leituras importantes desses números:

  1. A duplicação por fator é exata na prática: 66 → 131 → 270 → 524 → 1.035 ms. A teoria do expoente (2^work factor) se confirma com precisão de relógio — cada +1 no fator dobra o tempo medido.
  1. Verify custa o mesmo que HashPassword, confirmado na medição: 258 ms contra 270 ms no fator 12 — a pequena diferença é a geração do salt, que só existe no HashPassword. A tese central do impacto em produção (cada login paga o preço cheio) está validada empiricamente.
  1. A mesma configuração, em hardware diferente, muda tudo: work factor 12 nesta máquina custa ~270 ms — dentro do alvo da OWASP. No Basic B2 do caso real, os mesmos 2^12 custavam ~2,3 s — 8,6x mais lento. O número que está perfeito na máquina do desenvolvedor estoura o orçamento de CPU no plano de entrada da nuvem.
  1. O PasswordHasher<T> com 100 mil iterações custa o mesmo que o BCrypt em fator 10 (~64 ms) — PBKDF2 se beneficia da aceleração de hardware para SHA nos processadores modernos, enquanto o BCrypt.Net-Next é 100% managed e não usa intrinsics.

Isso não significa que PBKDF2 é “melhor” — significa que cada algoritmo tem seu perfil de custo e que o custo precisa ser uma decisão consciente, medida no hardware real. A ordem de preferência atual da OWASP para armazenamento de senhas é: Argon2id (primeira escolha), scrypt (alternativa), bcrypt para sistemas legados (com work factor mínimo 10 e limite de 72 bytes de senha) e PBKDF2 quando há exigência de conformidade FIPS-140 (com 600.000 iterações ou mais para HMAC-SHA256). Trocar de algoritmo é decisão de arquitetura de segurança, não de performance — e não é o que este caso pedia.

Como corrigir sem abrir mão da segurança

As correções abaixo estão em ordem de custo-benefício — e a primeira custa uma linha.

Calibre o work factor para o hardware de produção

A recomendação da OWASP é explícita: work factor mínimo de 10, e acima disso “tão alto quanto a performance do servidor de verificação permitir” — com a regra geral de que calcular um hash deve levar menos de um segundo, e o alvo operacional saudável fica na faixa de 250–500 ms.

A palavra importante é medir. O processo de calibração do work factor em cinco passos:

  1. Rode a medição no hardware de produção, ou em hardware idêntico — nunca na máquina de desenvolvimento.
  2. Meça o tempo médio de pelo menos 5 hashes por fator, após aquecer o JIT.
  3. Escolha o maior fator que mantenha o hash entre 250 e 500 ms.
  4. Nunca desça abaixo de 10 — é o mínimo da OWASP para BCrypt.
  5. Recalibre a cada mudança de plano, hardware ou região.

O código para a medição:

No caso do B2, a conta fechou assim: com work factor 12 o hash levava ~2,3 s — quase 10x acima do alvo. Baixando para 10, o custo cai 4x (~575 ms), dentro da regra de “menos de um segundo” da OWASP e ainda com 1.024 rodadas de proteção real contra força bruta.

E aqui volta o detalhe do formato do hash: baixar o work factor não quebra nenhuma senha existente. Como cada hash carrega o próprio fator embutido, Verify continua validando os hashes antigos (gerados com 12) normalmente; apenas os novos nascem com o fator calibrado. Melhor ainda: dá para re-hashear no próximo login, usando a API PasswordNeedsRehash do BCrypt.Net-Next:

Atenção a um detalhe da semântica: PasswordNeedsRehash(hash, novoFator) retorna true quando o fator do hash é menor que o novo mínimo. No cenário de downgrade (12 → 10), os hashes antigos continuam válidos e mais fortes que o novo padrão — não há necessidade de re-hasheá-los, e mantê-los custa apenas o tempo extra de Verify no login daquele usuário. A migração transparente brilha mesmo no caminho inverso: quando você escalar a infraestrutura e quiser subir o fator de volta.

Limite a concorrência de hashing com SemaphoreSlim

Calibrar o work factor resolve o custo unitário. Mas ainda falta isolar o dano: nada impede 10 verificações BCrypt simultâneas de pinar todos os núcleos. A solução é um portão de concorrência — no máximo Environment.ProcessorCount hashes ao mesmo tempo (ou menos, para reservar CPU para o resto da aplicação), e o excedente espera na fila ou recebe fast-fail:

E no handler, com resposta HTTP honesta quando a capacidade esgota:

Registre o portão como singleton — o limite é do processo, não do request:

Dois pontos importantes sobre esse padrão:

  • Use WaitAsync, nunca Wait — a espera na fila não pode bloquear a thread do pool, senão você troca saturação de CPU por thread pool starvation.
  • O Task.Run aqui não torna o hash mais rápido — ele continua custando o mesmo tempo de CPU. O que o portão faz é isolar o dano: com 2 núcleos e limite 1, o pior cenário passa a ser 1 núcleo dedicado a hashing e 1 núcleo livre para o resto da API. O colapso total deixa de ser possível.

Combine com lockout por conta

O rate limiter por IP barra um atacante único; o lockout por conta (bloqueio temporário após N tentativas falhas) reduz a superfície de tentativas válidas contra uma mesma conta, venham de onde vierem. Menos tentativas processadas = menos CPU queimada com Verify de senhas erradas. No ASP.NET Core Identity isso já vem pronto (LockoutOptions); em implementações próprias, um contador com expiração no cache distribuído resolve.

Resolver com infraestrutura custa caro todos os meses

Existe uma quarta alavanca, legítima: escalar. Mais núcleos = mais hashes simultâneos sem saturar. Mas ela merece uma análise fria, porque tem uma diferença fundamental em relação às anteriores: as correções de código custam zero e são permanentes; a infraestrutura custa todos os meses, para sempre.

Com os preços oficiais do Azure App Service para Linux (pay-as-you-go, sem instância reservada, região Brazil South, consultados na Azure Retail Prices API em julho de 2026 — 730 h/mês):

PlanovCPUs / RAMUSD/mêsUSD/12 meses
Basic B2 (atual)2 / 3,5 GB~30~359
Basic B34 / 7 GB~59~710
Premium P1v32 / 8 GB~162~1.945

Em 12 meses, a decisão fica assim:

EstratégiaCusto em 12 mesesO que resolve
Calibrar work factor (12 → 10)US$ 0Corta 4x o custo de CPU por autenticação
SemaphoreSlim + fast-failUS$ 0 (~30 linhas)Elimina o colapso total sob pico
Scale-up B2 → B3+ US$ 351/anoDobra os núcleos; o hash continua custando 2,3 s
Scale-up B2 → P1v3+ US$ 1.586/anoNúcleos mais rápidos e always on; causa raiz intacta

Na região East US, os mesmos planos custam menos (B2 ~US$ 298/ano, B3 ~US$ 587/ano, P1v3 ~US$ 1.358/ano), e nos planos Windows os valores são substancialmente maiores — mas a proporção da análise não muda.

A leitura correta da tabela não é “nunca escale”. É a ordem das alavancas:

  1. Primeiro o código: com o work factor calibrado e o portão de concorrência, o B2 volta a ser suficiente para a carga atual — de graça.
  2. Depois a infraestrutura, se o negócio pedir: quando o tráfego crescer de verdade, o upgrade compra capacidade real — e aí sim vale pagar. Detalhe importante: ao escalar, recalibre o work factor para cima. Núcleos mais rápidos permitem voltar ao fator 12 dentro do alvo de 250–500 ms, e a migração transparente via PasswordNeedsRehash promove os hashes no login seguinte, sem nenhuma ação do usuário.

Escalar sem corrigir o código é o pior dos mundos: você paga US$ 1.586 a mais por ano para continuar com um endpoint de login que consome segundos de CPU por chamada — só que agora com mais núcleos para saturar.

O equilíbrio certo entre segurança e disponibilidade

O work factor do BCrypt não é “quanto maior, melhor”. É um trade-off explícito entre resistência a força bruta (fator mais alto) e disponibilidade (fator mais alto = mais CPU por autenticação). Num servidor de 2 núcleos, o ganho marginal de segurança do fator 12 sobre o 10 não paga o risco de derrubar a API inteira — e a própria OWASP trata o assunto como calibração para o hardware alvo, não como número mágico.

O resumo operacional do caso:

  • Correção mínima e imediata: baixar o work factor de 12 para 10 — uma linha — cortou o tempo e a CPU por autenticação em 4x e eliminou o pior do risco.
  • Blindagem: o SemaphoreSlim com fast-fail garante que, mesmo num pico anômalo, o dano fica contido no subsistema de autenticação.
  • Evolução: quando escalar, recalibrar para cima e re-hashear no login. Segurança acompanha capacidade — nos dois sentidos.

Hash de senha com BCrypt é a única parte do seu sistema que você configura, de propósito, para ser lenta. Trate essa lentidão como o recurso finito que ela é: meça no hardware real, limite a concorrência e reavalie a calibração a cada mudança de infraestrutura.

FAQ

1. O BCrypt ainda é seguro em 2026?

Sim. O BCrypt continua criptograficamente sólido e amplamente usado. A OWASP o classifica hoje como opção para sistemas legados — para projetos novos, a primeira recomendação é Argon2id, seguido de scrypt. Se o seu sistema já usa BCrypt com work factor 10 ou mais, não há urgência de migração — há urgência de calibração.

2. Qual work factor devo usar no BCrypt?

O que a medição mandar. A regra da OWASP: mínimo 10, e acima disso o máximo que o hardware de produção suportar mantendo o hash abaixo de 1 segundo — o alvo saudável fica entre 250 e 500 ms. Meça no servidor real: a diferença para a máquina de desenvolvimento passou de 8x no caso deste artigo.

3. Baixar o work factor de 12 para 10 enfraquece as senhas já armazenadas?

Não. Cada hash BCrypt carrega o próprio work factor embutido na string ($2b$12$...), e o Verify usa o fator do hash armazenado — os hashes antigos continuam válidos e com a força original. Apenas os novos nascem com o fator calibrado, e PasswordNeedsRehash permite promovê-los depois, no login, de forma transparente.

4. Verify é mais leve que HashPassword?

Não. Para comparar a senha, o Verify extrai o salt do hash armazenado e re-executa o KDF completo, com exatamente o mesmo custo de CPU do HashPassword no mesmo fator. Como login é muito mais frequente que cadastro ou reset, é o Verify que domina o consumo de CPU de autenticação do sistema.

5. Usar async ou Task.Run resolve a lentidão do BCrypt?

Não. BCrypt é trabalho CPU-bound puro: async/await liberam threads durante esperas de I/O, mas aqui não há espera — há processamento. Task.Run apenas move o custo para outra thread do mesmo processador. O que resolve é calibrar o work factor e limitar a concorrência com SemaphoreSlim.

6. Devo trocar o BCrypt pelo PasswordHasher do ASP.NET Core Identity?

Depende do contexto. O PasswordHasher<T> (PBKDF2 com HMAC-SHA512 e 100 mil iterações desde o .NET 7) é mais rápido por hash, mas PBKDF2 é menos resistente a ataques com GPU que BCrypt e Argon2id. Se você já tem uma base de hashes BCrypt funcionando, calibrar o work factor é a correção certa; para sistemas novos, avalie Argon2id primeiro.

Compartilhe:

Tiago Tartari

Tiago Tartari

Eu ajudo e capacito pessoas e organizações a transformar problemas complexos em soluções práticas usando a tecnologia para atingir resultados extraordinários.

Qual é o desafio
que você tem hoje?