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.
Measuring Your Actual Hashing Time
Before settling on a cost factor, measure how long hashing takes on your actual production server. This matters because shared hosting environments, virtual private servers, and dedicated hardware all perform differently. A cost factor that produces acceptable times on one server might cause noticeable delays on another.
A simple PHP script can measure hashing time:
$start = microtime(true);
$hash = password_hash('test_password_123', PASSWORD_BCRYPT, ['cost' => 12]);
$end = microtime(true);
$time_ms = ($end - $start) * 1000;
echo "Hashing took: " . round($time_ms, 2) . " ms";
Run this on your production server, not your local development machine. The difference can be significant. If cost factor 12 takes 50 milliseconds on shared hosting but 2 milliseconds on a dedicated server, your cost factor choice should reflect the hardware you actually use.
Regular measurement also helps you plan future upgrades. If your current hardware produces 50-millisecond hashes with cost factor 12, you have limited headroom for increasing the cost factor as hardware ages. Document your baseline measurements so you can compare them during server upgrades or when evaluating whether to increase security.
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.
When planning a cost factor increase, consider doing it gradually or during low-traffic periods. If you have a large user base and switch to a significantly higher cost factor, the first wave of logins after deployment will all trigger rehashing, which can create a temporary spike in server load. This is rarely a problem, but it is worth knowing before you deploy.
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.
If you implement SHA-256 preprocessing, the code looks like this:
// For applications requiring longer password support
$input = strlen($password) > 72 ? hash('sha256', $password, true) : $password;
$hash = password_hash($input, PASSWORD_BCRYPT, ['cost' => 12]);
Note the second parameter in hash() set to true. This returns raw binary output rather than a hexadecimal string, which keeps the input within BCrypt's 72-byte limit.
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.
Consider checking passwords during login as well, not just registration. If a user's password appears in a breach at any point, they should be notified and prompted to change it. Some applications display a warning on the profile page if the password has appeared in breach databases.
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.
CSRF Protection in Your Login and Registration Forms
While BCrypt handles password storage securely, your login and registration forms need protection against cross-site request forgery. An attacker can craft a form on a malicious site that submits a login request to your application using the victim's stored credentials. Without CSRF protection, the attacker's form can authenticate as the victim without the victim's knowledge.
PHP applications should implement anti-CSRF tokens on all state-changing forms, including login and registration pages. The token is generated per session, embedded in the form, and validated on submission. This ensures that only forms originating from your legitimate pages can submit successfully.
For a practical implementation guide covering token generation, storage, and validation in PHP, see the CSRF protection in PHP guide, which walks through building secure token handling into your existing forms without relying on external libraries.
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.
The key advantage of Argon2id is its resistance to specialised hardware attacks. It also allows tuning of memory usage alongside time cost, which makes it harder to optimise attacks using GPU or ASIC hardware. BCrypt's fixed memory requirements make it less resistant to certain types of hardware acceleration, though it remains appropriate for most web applications.
If you decide to migrate from BCrypt to Argon2id, you can use the same password_needs_rehash() approach to upgrade hashes during user login, gradually moving your entire user base to the new algorithm without downtime or mass-rehashing operations.
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.
Set secure cookie flags on your session cookies. The secure flag ensures cookies are only transmitted over HTTPS, and the httponly flag prevents JavaScript from accessing the cookie value. Both settings reduce the risk of session theft through cross-site scripting or network interception.
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
Regenerating the session ID after successful login prevents session fixation attacks where an attacker sets a known session ID before the victim authenticates. Call session_regenerate_id(true) immediately after verifying credentials.
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.
Regular Security Reviews for Your Authentication Stack
Setting up BCrypt correctly once is not a one-time task. Authentication security requires ongoing attention as threats evolve and hardware improves. Schedule periodic reviews of your password hashing configuration, session management practices, and breach detection mechanisms.
During a security review, check whether your current cost factor still provides adequate protection given your server hardware. Review whether any of your dependencies that handle authentication have known vulnerabilities. Check your logs for suspicious login patterns that might indicate credential stuffing attempts.
Keeping your PHP version up to date matters as well. Newer PHP versions often include performance improvements for password hashing functions, which can allow you to increase the cost factor without increasing actual login latency. Running an outdated PHP version may also mean missing out on security fixes related to session management or cryptographic functions.
Related practical reading
These related guides can help you connect this topic with the wider website, server, security, and support decisions around it.
- SSH Config Tips That Save Hours of Time - useful background for related technology decisions