Choosing the right BCrypt cost factor affects both the security of your stored passwords and the performance of your login system. Set it too low and your hashes become vulnerable to brute-force attacks. Set it too high and your users experience slow login pages, timeouts, or excessive server load. This guide walks through what the cost factor controls, how to choose an appropriate value, and how to implement it correctly in PHP.
What Makes BCrypt Different from MD5 and SHA-1
MD5 and SHA-1 were designed for fast data integrity checks, not password storage. A modern GPU can compute billions of MD5 or SHA-1 hashes per second. When a database of password hashes is leaked, an attacker can test every possible password combination against those hashes in a matter of hours. Most passwords found in common dictionaries fall quickly.
BCrypt was designed with password storage in mind. It is deliberately slow, and that slowness is the point. The algorithm runs a configurable number of computational iterations, determined by the cost factor. Doubling the cost factor roughly doubles the time needed to hash a password. A BCrypt hash that takes 250 milliseconds to compute is approximately 100,000 times harder to brute-force than an MD5 hash that takes 2.5 microseconds.
BCrypt is also adaptive. As hardware improves, you can increase the cost factor to maintain the same effective security level. A cost factor chosen today that produces a 100-millisecond hashing time can be increased in future hardware generations to keep attack difficulty consistently high.
If you are building or maintaining a PHP application that handles user authentication, a solid understanding of BCrypt and its cost factor helps you make informed decisions about password security rather than relying on defaults you have not tested.
How the BCrypt Cost Factor Works
The BCrypt cost factor is an integer value, typically ranging from 10 to 14 for most web applications. The relationship is logarithmic: each increment in the cost factor roughly doubles the computation time.
Approximate hashing times on typical server hardware:
- Cost factor 10: roughly 1 millisecond per hash
- Cost factor 11: roughly 2 milliseconds per hash
- Cost factor 12: roughly 4 milliseconds per hash
- Cost factor 13: roughly 8 milliseconds per hash
- Cost factor 14: roughly 16 milliseconds per hash
These numbers vary depending on your server hardware. A fast development machine will produce shorter times than a budget shared hosting environment. Before deploying any cost factor to production, measure the actual hashing time on the server where your application runs.
For most web applications, cost factor 12 is a reasonable default on modern servers. High-security applications such as password managers or financial systems may warrant cost factor 13 or 14, provided your server can handle the additional load without degrading login performance. For applications where login speed is more critical than maximum password hash strength, cost factor 10 remains acceptable.
Implementing BCrypt Password Hashing in PHP
PHP provides password_hash() and password_verify() as built-in functions. These abstract the algorithm details and make secure password storage straightforward to implement.
// Hashing a password with BCrypt
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// Verifying a password
if (password_verify($password, $hash)) {
// Password is correct
}
Store the complete hash string in your database. The stored value includes the algorithm identifier, the cost factor used, the salt, and the hash output. The password_verify() function reads all of these values automatically when you pass the stored hash.
PHP generates a cryptographically random salt automatically when you call password_hash(). You do not need to generate or manage salts separately. Using a fixed salt or one derived from user data reduces the randomness that makes each hash unique, even when the same password is used by different accounts.
Upgrading Hashes When You Change the Cost Factor
As your servers age and hardware improves, you will want to increase the cost factor to maintain consistent security levels. PHP's password_needs_rehash() function checks whether an existing hash was computed with the current cost factor.
if (password_verify($password, $hash)) {
if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) {
$new_hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// Update the hash in your database
update_user_hash($user_id, $new_hash);
}
return true;
}
This approach rehashing only occurs when a user logs in, and only for the authenticated user. It avoids any mass-rehashing operation across your entire user database, which could cause significant server load.
Password Length Limits and Edge Cases
BCrypt has a maximum input length of 72 bytes. For most applications, this is not a practical concern because very few users have passwords longer than 72 characters. However, if you allow passphrases or long passwords, you may need to handle this limit.
One common approach is to hash long passwords with SHA-256 first and use that hash as the input to BCrypt. This extends the practical maximum while preserving BCrypt's slow hashing properties. A simpler alternative is to enforce a maximum password length of 72 characters, which accommodates most passphrases comfortably while preventing denial-of-service attacks that use extremely long passwords to consume hashing resources.
What Happens When Password Hashes Are Leaked
If your database of password hashes is exposed, the primary risk is that attackers will recover the original passwords and try them on other services. Many users reuse passwords across different sites, so a breach of your hashes can compromise accounts on other platforms. This practice is known as credential stuffing.
Mitigations include preventing leaks in the first place through access controls, encryption at rest, and monitoring. Using a slow hashing algorithm like BCrypt significantly increases the time and cost required to crack the hashes. Detecting credential stuffing attacks early by monitoring login attempts against known breach databases is also worthwhile.
If you discover a breach of password hashes, treat it as a serious incident. Assume that some passwords will be recovered by attackers. Force password resets for affected accounts, notify your supervisory authority if you are subject to UK GDPR requirements, and inform affected users with clear guidance on what happened and what they should do next, including changing their passwords on your service and any other platform where they used the same credentials.
Checking Passwords Against Known Breaches
The Have I Been Pwned (HIBP) Passwords API lets you check whether a password appears in a known breach database without transmitting the actual password. The API uses k-Anonymity: you send only the first five characters of the SHA-1 hash of the password, and HIBP returns all hash prefixes matching that range. Your application then checks whether the full hash appears in the response.
function password_in_breach(string $password): bool {
$hash = strtoupper(sha1($password));
$prefix = substr($hash, 0, 5);
$ch = curl_init('https://api.pwnedpasswords.com/range/' . $prefix);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return str_contains($response, substr($hash, 5));
}
This approach means HIBP never sees the full password or hash, and you never send the actual password over the network. You can check new user passwords during registration and prompt users to choose a different password if it appears in a known breach.
Password Policy That Actually Helps
Enforcing complex password requirements (uppercase letters, numbers, special characters) often backfires. Users respond predictably by choosing patterns like Password1! and reusing similar structures across services. Modern guidance from the National Institute of Standards and Technology (NIST) recommends allowing any characters, enforcing a minimum length of at least 8 characters, and checking new passwords against known breach databases rather than requiring specific character types.
A 16-character passphrase tends to be more memorable and more secure than an 8-character password with complexity requirements. The search space is larger even though the character set is smaller. Passphrases like correct horse battery staple are easier for users to remember and harder for attackers to crack than P@ssw0rd!
Beyond password hashing, securing authenticated sessions is a separate but equally important layer of your application security. Managing sessions securely after verifying a password helps protect against session fixation and session hijacking attacks.
Argon2 as an Alternative to BCrypt
Argon2, specifically Argon2id, is a newer algorithm that won the Password Hashing Competition in 2015. It is considered state-of-the-art and particularly resistant to GPU-based attacks. PHP supports Argon2id via the PASSWORD_ARGON2ID constant in PHP 7.3 and later.
$hash = password_hash($password, PASSWORD_ARGON2ID, ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3]);
For new projects, Argon2id is a reasonable choice. For existing projects already using BCrypt securely, migrating to Argon2id is worth evaluating but BCrypt remains secure and does not require urgent replacement. If you are starting a new PHP project today and want the most current recommendation, Argon2id is a sensible default.
Session Security After Authentication
Password hashing protects stored credentials, but it is only one part of authentication security. Once a user logs in, you need to manage their session securely. This involves using secure session cookies, regenerating session IDs after login, implementing appropriate session timeouts, and protecting against session fixation and hijacking attacks.
Securing the login endpoint itself also matters. Ensuring your forms are protected against cross-site request forgery helps prevent attackers from tricking users into submitting authenticated requests without their knowledge. A thorough approach to authentication security addresses both the password storage layer and the session management layer.