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
- 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.
Verifycusta exatamente o mesmo queHashPasswordno 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.- Hash de senha é CPU-bound puro e não existe
awaitque 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. - 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.
- 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 factor | Rodadas | Custo relativo |
|---|---|---|
| 10 | 1.024 | 1x |
| 11 | 2.048 | 2x |
| 12 | 4.096 | 4x |
| 13 | 8.192 | 8x |
| 14 | 16.384 | 16x |
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:
|
1 2 3 4 5 6 7 8 9 10 |
using BC = BCrypt.Net.BCrypt; // Gera salt automaticamente; work factor padrão da biblioteca = 11 string hash = BC.HashPassword("senha-do-usuario"); // Ou com work factor explícito string hashWf12 = BC.HashPassword("senha-do-usuario", workFactor: 12); // Validação bool ok = BC.Verify("senha-do-usuario", hashWf12); |
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:
|
1 2 3 4 5 |
$2b$12$N9qo8uLOickgx2ZMRZoMye.IjPeGvGzjF1Q8yPqYAP3v6JmKZWvHe │ │ └──────────┬─────────┘└────────────┬────────────────┘ │ │ salt (22 chars) hash (31 chars) │ └── work factor (12) └── versão do algoritmo (2b) |
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
awaitque 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:
|
1 2 3 4 5 6 7 8 9 10 |
public class PasswordService : IPasswordService { private const int WorkFactor = 12; public string HashPassword(string password) => BCrypt.Net.BCrypt.HashPassword(password, WorkFactor); public bool VerifyPassword(string password, string hash) => BCrypt.Net.BCrypt.Verify(password, hash); } |
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:
| Fluxo | Operação BCrypt | Custo no B2 |
|---|---|---|
| Login | Verify | ~2,3 s por login |
| Login de usuário legado (primeira vez) | Verify legado + HashPassword (migração) | ~4,6 s |
| Cadastro de cliente | HashPassword | ~2,3 s |
| Reset de senha | HashPassword | ~2,3 s |
| Troca de senha | Verify + 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:
- 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.
- 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.
- 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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using Microsoft.AspNetCore.Identity; using BC = BCrypt.Net.BCrypt; BenchmarkRunner.Run<PasswordHashBenchmarks>(); [ShortRunJob] [MemoryDiagnoser] public class PasswordHashBenchmarks { private const string Password = "S3nh@F0rte!Exemplo2026"; private readonly PasswordHasher<object> _identityHasher = new(); private readonly object _user = new(); private string _bcryptHash10 = null!; private string _bcryptHash12 = null!; private string _identityHash = null!; [GlobalSetup] public void Setup() { _bcryptHash10 = BC.HashPassword(Password, 10); _bcryptHash12 = BC.HashPassword(Password, 12); _identityHash = _identityHasher.HashPassword(_user, Password); } [Benchmark] public string BCrypt_Hash_WF10() => BC.HashPassword(Password, 10); [Benchmark] public string BCrypt_Hash_WF11() => BC.HashPassword(Password, 11); [Benchmark] public string BCrypt_Hash_WF12() => BC.HashPassword(Password, 12); [Benchmark] public string BCrypt_Hash_WF13() => BC.HashPassword(Password, 13); [Benchmark] public string BCrypt_Hash_WF14() => BC.HashPassword(Password, 14); [Benchmark] public bool BCrypt_Verify_WF10() => BC.Verify(Password, _bcryptHash10); [Benchmark] public bool BCrypt_Verify_WF12() => BC.Verify(Password, _bcryptHash12); [Benchmark] public string Identity_PBKDF2_Hash() => _identityHasher.HashPassword(_user, Password); [Benchmark] public PasswordVerificationResult Identity_PBKDF2_Verify() => _identityHasher.VerifyHashedPassword(_user, _identityHash, Password); } |
Resultado na minha máquina de desenvolvimento (Intel Core i7-8665U, 4 núcleos físicos, .NET 10.0.1, build Release):
| Método | Média | Desvio padrão | Alocação |
|---|---|---|---|
| BCrypt_Hash_WF10 | 66,58 ms | 1,338 ms | 5.448 B |
| BCrypt_Hash_WF11 | 130,71 ms | 2,669 ms | 5.460 B |
| BCrypt_Hash_WF12 | 269,62 ms | 4,944 ms | 5.448 B |
| BCrypt_Hash_WF13 | 523,81 ms | 7,803 ms | 5.448 B |
| BCrypt_Hash_WF14 | 1.035,31 ms | 7,648 ms | 5.448 B |
| BCrypt_Verify_WF10 | 66,46 ms | 1,757 ms | 5.264 B |
| BCrypt_Verify_WF12 | 258,16 ms | 7,043 ms | 5.264 B |
| Identity_PBKDF2_Hash | 63,60 ms | 3,653 ms | 436 B |
| Identity_PBKDF2_Verify | 62,67 ms | 2,761 ms | 297 B |
Quatro leituras importantes desses números:
- 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.
Verifycusta o mesmo queHashPassword, confirmado na medição: 258 ms contra 270 ms no fator 12 — a pequena diferença é a geração do salt, que só existe noHashPassword. A tese central do impacto em produção (cada login paga o preço cheio) está validada empiricamente.
- 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.
- 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:
- Rode a medição no hardware de produção, ou em hardware idêntico — nunca na máquina de desenvolvimento.
- Meça o tempo médio de pelo menos 5 hashes por fator, após aquecer o JIT.
- Escolha o maior fator que mantenha o hash entre 250 e 500 ms.
- Nunca desça abaixo de 10 — é o mínimo da OWASP para BCrypt.
- Recalibre a cada mudança de plano, hardware ou região.
O código para a medição:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System.Diagnostics; using BC = BCrypt.Net.BCrypt; // Rode isto no hardware de PRODUÇÃO (ou idêntico a ele) const string senhaDeTeste = "S3nh@F0rte!Exemplo2026"; for (int workFactor = 10; workFactor <= 14; workFactor++) { // Aquecimento (JIT) BC.HashPassword(senhaDeTeste, workFactor); var sw = Stopwatch.StartNew(); const int amostras = 5; for (int i = 0; i < amostras; i++) BC.HashPassword(senhaDeTeste, workFactor); sw.Stop(); Console.WriteLine( $"WF {workFactor}: {sw.ElapsedMilliseconds / amostras} ms por hash"); } |
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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class PasswordService : IPasswordService { // Calibrado por medição no hardware de produção (B2): ~575 ms por hash. // Recalibrar se o plano do App Service mudar. private const int WorkFactor = 10; public string HashPassword(string password) => BCrypt.Net.BCrypt.HashPassword(password, WorkFactor); public bool VerifyPassword(string password, string hash) => BCrypt.Net.BCrypt.Verify(password, hash); public bool NeedsRehash(string hash) => BCrypt.Net.BCrypt.PasswordNeedsRehash(hash, WorkFactor); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public async Task<LoginResult> Handle(LoginCommand command) { var user = await _users.FindByEmailAsync(command.Email); if (user is null || !_passwords.VerifyPassword(command.Password, user.PasswordHash)) return LoginResult.InvalidCredentials(); // Migração transparente: re-hasheia com o fator calibrado // A senha em texto só existe aqui, durante o login — momento perfeito if (_passwords.NeedsRehash(user.PasswordHash)) { user.PasswordHash = _passwords.HashPassword(command.Password); await _users.UpdateAsync(user); } return LoginResult.Success(user); } |
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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public sealed class PasswordHashingGate : IDisposable { private readonly SemaphoreSlim _gate; private readonly TimeSpan _timeout; public PasswordHashingGate(int? maxConcurrency = null, TimeSpan? timeout = null) { // Reserva pelo menos 1 núcleo para o resto da aplicação var limit = maxConcurrency ?? Math.Max(1, Environment.ProcessorCount - 1); _gate = new SemaphoreSlim(limit, limit); _timeout = timeout ?? TimeSpan.FromSeconds(5); } public async Task<T> RunAsync<T>(Func<T> cpuBoundWork, CancellationToken ct = default) { // WaitAsync: a thread NÃO bloqueia enquanto espera na fila if (!await _gate.WaitAsync(_timeout, ct)) throw new HashingCapacityException( "Capacidade de hashing esgotada. Tente novamente em instantes."); try { // Task.Run move o trabalho CPU-bound para fora da thread do request return await Task.Run(cpuBoundWork, ct); } finally { _gate.Release(); } } public void Dispose() => _gate.Dispose(); } |
E no handler, com resposta HTTP honesta quando a capacidade esgota:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
app.MapPost("/auth/login", async ( LoginRequest request, PasswordHashingGate gate, IUserRepository users, IPasswordService passwords) => { var user = await users.FindByEmailAsync(request.Email); if (user is null) return Results.Unauthorized(); try { var valid = await gate.RunAsync( () => passwords.VerifyPassword(request.Password, user.PasswordHash)); return valid ? Results.Ok(TokenFor(user)) : Results.Unauthorized(); } catch (HashingCapacityException) { // 503 + Retry-After: melhor um login adiado que uma API derrubada return Results.StatusCode(StatusCodes.Status503ServiceUnavailable); } }); |
Registre o portão como singleton — o limite é do processo, não do request:
|
1 |
builder.Services.AddSingleton<PasswordHashingGate>(); |
Dois pontos importantes sobre esse padrão:
- Use
WaitAsync, nuncaWait— 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.Runaqui 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):
| Plano | vCPUs / RAM | USD/mês | USD/12 meses |
|---|---|---|---|
| Basic B2 (atual) | 2 / 3,5 GB | ~30 | ~359 |
| Basic B3 | 4 / 7 GB | ~59 | ~710 |
| Premium P1v3 | 2 / 8 GB | ~162 | ~1.945 |
Em 12 meses, a decisão fica assim:
| Estratégia | Custo em 12 meses | O que resolve |
|---|---|---|
| Calibrar work factor (12 → 10) | US$ 0 | Corta 4x o custo de CPU por autenticação |
SemaphoreSlim + fast-fail | US$ 0 (~30 linhas) | Elimina o colapso total sob pico |
| Scale-up B2 → B3 | + US$ 351/ano | Dobra os núcleos; o hash continua custando 2,3 s |
| Scale-up B2 → P1v3 | + US$ 1.586/ano | Nú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:
- 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.
- 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
PasswordNeedsRehashpromove 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
SemaphoreSlimcom 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.