Why BCrypt Cost Factor Matters More for PHP Passwords

15 min read 2,961 words
Why BCrypt Cost Factor Matters More Than You Think for PHP Password Hashing featured image

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.

Frequently Asked Questions

Is Argon2 better than BCrypt?
Argon2id, the recommended variant of Argon2, is more recent and offers specific advantages in resistance to GPU-based and hardware-based attacks. However, BCrypt remains secure and is widely deployed. For new projects, Argon2id is a reasonable choice. For existing projects using BCrypt, there is no urgent need to migrate unless you are reviewing your security stack anyway. Both algorithms are considerably stronger than MD5 or SHA-1.
Should I use a lower cost factor to improve login performance?
Only if performance problems are real and measured, not hypothetical. A login that takes 200 milliseconds instead of 100 milliseconds is not a meaningful user experience issue. A login page that takes 5 seconds is. Measure your actual login latency and identify the genuine bottleneck before changing the cost factor. Sometimes the issue lies elsewhere, such as in database queries, network latency, or external API calls rather than password hashing.
Should I implement a maximum password length?
Yes, but set it high enough to be practical. A maximum of 128 characters accommodates most passphrases comfortably. Setting a maximum prevents denial-of-service attacks that use extremely long passwords to consume hashing resources. BCrypt's 72-byte input limit is one reason to consider SHA-256 preprocessing for applications that need to handle very long passphrases, though this is uncommon for most use cases.
Does client-side password hashing add security?
Not meaningfully. The server-side hash is what matters for authentication because the server verifies the hash against the stored value, not the original password. If an attacker can intercept the hash in transit, they can use it to authenticate directly, regardless of whether the original password was hashed in the browser. Use HTTPS to protect credentials during transmission instead of relying on client-side hashing as a security measure.
How do I know if my current cost factor needs adjustment?
Measure your current hashing time on production hardware. If it falls below 100 milliseconds for cost factor 12 on a modern server, you have headroom to increase the cost factor in future. Review your cost factor every few years or whenever you upgrade server hardware. As a baseline, aim for a hashing time that makes brute-force attacks impractical within any useful timeframe, while keeping login performance acceptable for users.
Can I use BCrypt for API authentication tokens?
BCrypt is designed for password hashing, not for generating or verifying API tokens. Tokens are typically random strings rather than user-chosen passwords, so they do not benefit from the computational slowness that makes BCrypt useful for passwords. For API authentication, use cryptographic random token generation and store them as-is or with a fast hash, depending on your security requirements. HMAC-based token verification or JWT tokens are more appropriate for most API authentication patterns.
What should I do if my PHP version does not support Argon2?
If you are running PHP 7.2 or earlier, Argon2 support is not available. In that case, BCrypt with an appropriate cost factor remains a solid choice. Consider upgrading to a supported PHP version if possible, as PHP 7.2 reached end of security support and newer versions include improvements to both password hashing and overall security. If you cannot upgrade immediately, continue using BCrypt and ensure your cost factor provides adequate protection for your threat model.