Bcrypt is a password hashing function designed to make offline password guessing more expensive than a fast digest such as MD5 or SHA-256. It generates a salt as part of the encoded result and exposes an adjustable work factor, but it is not a universal guarantee against account takeover: password quality, rate limiting, breach response, transport security, and deployment calibration still matter.

Table of Contents

Key Takeaways

  • Built-in Salt: Bcrypt automatically generates a unique 128-bit salt for each password, eliminating the need for manual salt management.
  • Adaptive Cost Factor: The work factor (rounds) can be increased over time to keep pace with hardware improvements.
  • Slow by Design: Bcrypt is intentionally computationally expensive, making brute-force attacks impractical.
  • Hash Structure: A Bcrypt hash contains the algorithm version, cost factor, salt, and hash in a single string.
  • Cost Factor: Choose it from measurements on the target hardware and expected concurrency; do not copy a universal millisecond target.
  • Algorithm Choice: Bcrypt is a compatibility option. Evaluate Argon2id for new systems when its memory, time, and parallelism controls fit the deployment.

What is Bcrypt?

Bcrypt is a password hashing function designed by Niels Provos and David Mazières in 1999, based on the Blowfish cipher. The name "bcrypt" comes from "Blowfish crypt," reflecting its cryptographic foundation.

Why Bcrypt Was Created

Traditional hash functions like MD5 and SHA-1 were designed to be fast, which is great for data integrity checks but terrible for password storage. A fast hash means attackers can try billions of password guesses per second.

Bcrypt solves this by being:

  1. Deliberately Slow: Each hash computation takes significant time
  2. Configurable: The slowness can be adjusted via the cost factor
  3. Salt-Integrated: Each password gets a unique salt automatically

Key Features

Feature Description
Algorithm Based on Blowfish cipher (Eksblowfish)
Encoded output 60 characters: version, cost, 16-byte salt, and 23-byte checksum encoding
Salt 128-bit, automatically generated
Cost Factor Configurable (4-31), exponential work increase
String Format $2a$, $2b$, or $2y$ prefix

How Bcrypt Works

Bcrypt's security comes from its unique approach to password hashing. Here's the process:

flowchart TD A[Password Input] --> B[Generate Random Salt] B --> C["Combine Password + Salt"] C --> D[Key Setup Phase] D --> E["Expensive Key Schedule (2^cost iterations)"] E --> F[Encrypt Magic Value] F --> G[Final Hash Output] style A fill:#e1f5fe,stroke:#01579b style B fill:#fff3e0,stroke:#e65100 style E fill:#fce4ec,stroke:#c2185b style G fill:#e8f5e9,stroke:#2e7d32

Step-by-Step Process

  1. Salt Generation: A cryptographically secure 128-bit random salt is generated
  2. Key Setup: The password and salt are used to initialize the Eksblowfish cipher
  3. Expensive Key Schedule: The key schedule is repeated 2^cost times
  4. Encryption: A magic value ("OrpheanBeholderScryDoubt") is encrypted 64 times
  5. Output: The salt and resulting hash are combined into the final string

Why Same Password Produces Different Hashes

A common question is: "Why does hashing the same password twice give different results?"

This is because Bcrypt generates a new random salt for each hash operation. The salt is then embedded in the output string, so verification can extract it and reproduce the same hash.

code
Password: "mypassword"

First hash:  $2b$10$N9qo8uLOickgx2ZMRZoMy.MqrqQb9lYz6H8Kj7OvBOyj5uYjiPWmu
Second hash: $2b$10$ZGdlbGVwaGFudHNhcmVjb.7xJ8L9KjQvMnOpRsTuVwXyZaBcDeFgH

Both are valid hashes of "mypassword" - different salts, same password!

Understanding the Cost Factor

The cost factor (also called "rounds" or "work factor") determines how computationally expensive the hashing process is. It's expressed as a power of 2.

Cost Factor Impact

Cost Factor Relative key-schedule work
4 2⁴
8 2⁸
10 2¹⁰
12 2¹²
14 2¹⁴
16 2¹⁶

These are work-factor relationships, not latency promises. Benchmark p50/p95 under expected login concurrency on the production hardware and runtime.

Choosing the Right Cost Factor

flowchart LR A[Start] --> B{"Hash Time Target?"} B -->|Within measured budget| C["Lower factor"] B -->|At measured target| D["Selected factor"] B -->|Over budget| E["Reduce or redesign"] C --> F["Good for High Traffic"] D --> G["Recommended Balance"] E --> H["Maximum Security"] style D fill:#e8f5e9,stroke:#2e7d32 style G fill:#e8f5e9,stroke:#2e7d32

Recommendations:

  • Development/Testing: use a lower value only in isolated test configuration.
  • Production: choose the highest value that meets measured availability and login-latency budgets.
  • Higher assurance: evaluate a larger value only after measuring CPU saturation, queueing, and denial-of-service exposure.

Upgrading Cost Factor Over Time

As hardware improves, you should increase the cost factor. Here's a strategy:

javascript
// Check if hash needs upgrading during login
async function loginAndUpgrade(password, storedHash) {
  const isValid = await bcrypt.compare(password, storedHash);
  
  if (isValid) {
    const currentCost = parseInt(storedHash.split('$')[2]);
    const targetCost = 12;
    
    if (currentCost < targetCost) {
      // Rehash with higher cost factor
      const newHash = await bcrypt.hash(password, targetCost);
      await updateUserHash(newHash);
    }
  }
  
  return isValid;
}

Bcrypt Hash Structure Breakdown

A Bcrypt hash string contains the version, cost, salt, and checksum needed by a compatible verifier. The salt and checksum use bcrypt's modified Base64 alphabet, not the standard RFC 4648 alphabet:

code
$2b$12$N9qo8uLOickgx2ZMRZoMyeKj7OvBOyj5uYjiPWmuabcdefghijk
│ │  │  │                     │
│ │  │  │                     └── Checksum encoding (31 characters)
│ │  │  └── Salt (22 characters)
│ │  └── Cost Factor (2 digits)
│ └── Algorithm Version
└── Prefix Marker

Algorithm Versions

Version Description
$2$ Original specification (obsolete)
$2a$ Fixed bugs, most common
$2b$ Fixed unsigned char bug (2014)
$2y$ PHP ecosystem variant; interoperability must be verified with the chosen library

Decoding a Real Hash

Let's break down this hash: $2b$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa

Component Value Meaning
Algorithm $2b$ Bcrypt version 2b
Cost 10 2^10 = 1,024 iterations
Salt vI8aWBnW3fID.ZQ4/zo1G. 22-character bcrypt-Base64 salt encoding
Checksum q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa 31-character bcrypt-Base64 checksum encoding

Bcrypt vs Other Algorithms

Comparison Table

Feature Bcrypt Argon2id Scrypt PBKDF2
Year 1999 2015 2009 2000
Memory Hard No Yes Yes No
Primary tunable costs CPU Memory, time, parallelism Memory, CPU Iterations / PRF
OWASP Password Storage role Legacy systems Preferred choice If Argon2id unavailable FIPS-oriented deployments
Important boundary 72-byte input limit in common implementations Must size memory safely under concurrency Parameters require deployment calibration Needs a high iteration count and approved PRF

When to Use Each

flowchart TD A[Choose Algorithm] --> B{New Project?} B -->|Yes| C{Memory Available?} B -->|No| D["Keep Current if Secure"] C -->|Yes| E[Argon2id] C -->|No| F[Bcrypt] D --> G{"Using MD5 or SHA?"} G -->|Yes| H["Migrate to Bcrypt or Argon2"] G -->|No| I["Increase Cost Factor if Needed"] style E fill:#e8f5e9,stroke:#2e7d32 style F fill:#e8f5e9,stroke:#2e7d32

Summary:

  • New projects: Consider Argon2id (if library support exists)
  • Existing projects: Bcrypt is still excellent
  • Legacy systems: Migrate from MD5/SHA to Bcrypt
  • Resource-constrained: Bcrypt (lower memory requirements)

Code Examples

Node.js (bcryptjs)

javascript
const bcrypt = require('bcryptjs');

// Generate hash
async function hashPassword(password) {
  const costFactor = 12; // example only; calibrate for the deployment
  const hash = await bcrypt.hash(password, costFactor);
  return hash;
}

// Verify password
async function verifyPassword(password, hash) {
  const isMatch = await bcrypt.compare(password, hash);
  return isMatch;
}

// Usage
async function main() {
  const password = 'test-only-password-from-a-fixture';
  
  // Hash the password
  const hash = await hashPassword(password);
  // Store hash in the protected credential store; do not log it.
  
  // Verify correct password
  const isValid = await verifyPassword(password, hash);
  console.log('Valid:', isValid); // true
  
  // Verify wrong password
  const isInvalid = await verifyPassword('wrongPassword', hash);
  console.log('Invalid:', isInvalid); // false
}

main();

Python

python
import bcrypt

def hash_password(password: str) -> bytes:
    """Hash a password with bcrypt."""
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
    return hashed

def verify_password(password: str, hashed: bytes) -> bool:
    """Verify a password against a hash."""
    return bcrypt.checkpw(password.encode('utf-8'), hashed)

# Usage
password = "test-only-password-from-a-fixture"

# Hash
hashed = hash_password(password)
# Store the hash in the protected credential store; do not log it.

# Verify
is_valid = verify_password(password, hashed)
print(f"Valid: {is_valid}")  # True

is_invalid = verify_password("wrongPassword", hashed)
print(f"Invalid: {is_invalid}")  # False

Java (Spring Security)

java
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BcryptExample {
    public static void main(String[] args) {
        // Create encoder with strength (cost factor) 12
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
        
        String password = "test-only-password-from-a-fixture";
        
        // Hash the password
        String hash = encoder.encode(password);
        // Store the hash in the protected credential store; do not log it.
        
        // Verify password
        boolean isValid = encoder.matches(password, hash);
        System.out.println("Valid: " + isValid); // true
        
        boolean isInvalid = encoder.matches("wrongPassword", hash);
        System.out.println("Invalid: " + isInvalid); // false
    }
}

Go

go
package main

import (
    "fmt"
    "golang.org/x/crypto/bcrypt"
)

func hashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
    return string(bytes), err
}

func verifyPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func main() {
    password := "test-only-password-from-a-fixture"
    
    // Hash
    hash, err := hashPassword(password)
    if err != nil {
        panic(err) // production code should return a structured error
    }
    // Store hash in the protected credential store; do not log it.
    
    // Verify
    isValid := verifyPassword(password, hash)
    fmt.Println("Valid:", isValid) // true
    
    isInvalid := verifyPassword("wrongPassword", hash)
    fmt.Println("Invalid:", isInvalid) // false
}

Security Best Practices

Do's ✅

  1. Calibrate the cost factor on production-like hardware and expected concurrency
  2. Store the complete hash string (includes salt and version)
  3. Use constant-time comparison (built into bcrypt libraries)
  4. Upgrade cost factor as hardware improves
  5. Use HTTPS when transmitting passwords
  6. Implement rate limiting on login endpoints

Don'ts ❌

  1. Don't copy a cost factor or latency target without measuring the deployment
  2. Don't store salt separately (it's in the hash)
  3. Don't silently truncate passwords before hashing
  4. Don't use bcrypt for non-password data (use SHA-256)
  5. Don't log passwords or hashes in plain text
  6. Don't implement bcrypt yourself (use established libraries)

Password Length Considerations

Bcrypt implementations commonly process only the first 72 bytes. Because multibyte encodings and NUL handling can create edge cases, define a byte-length policy and reject or explicitly version any longer-password scheme rather than silently truncating it:

javascript
const bcrypt = require('bcryptjs');

async function hashPasswordWithLengthPolicy(password) {
  if (Buffer.byteLength(password, 'utf8') > 72) {
    throw new Error('password_too_long_for_bcrypt_policy');
  }
  const costFactor = 12; // calibrate this value for the deployment
  return bcrypt.hash(password, costFactor);
}

If a system deliberately pre-hashes, it needs a documented, domain-separated, versioned construction and a migration plan; it must not be introduced as an invisible compatibility fix.

Use the Bcrypt Generator and Verifier only with test fixtures to inspect the encoded hash or exercise a compare operation. Production authentication must run in the controlled application environment with the maintained server-side library, calibrated cost, rate limits, and credential-handling controls described above. The hash glossary distinguishes password hashing from general-purpose digests.

FAQ

Q1: Why does the same password produce different hashes?

Bcrypt generates a unique random salt for each hash operation. This salt is embedded in the output string. During verification, the salt is extracted and used to reproduce the hash. This design prevents rainbow table attacks.

Q2: What cost factor should I use?

There is no universal value. Test the selected library on production-like hardware, measure p50/p95 under expected concurrency, and choose the highest factor that meets the service's availability and login-latency budgets.

Q3: Is Bcrypt still secure in 2026?

Bcrypt remains a viable compatibility option when implemented with a maintained library, a calibrated work factor, rate limits, and breach-response controls. For new systems, evaluate Argon2id when its memory, time, and parallelism parameters fit the deployment; verify current guidance and library support rather than treating any algorithm as a guarantee.

Q4: Can I use Bcrypt for API keys or tokens?

No, Bcrypt is designed for password verification, not general hashing. For API keys or tokens, use SHA-256 or HMAC-SHA256. Bcrypt's slowness would create performance issues for high-frequency operations.

Q5: How do I migrate from MD5 to Bcrypt?

During user login, use an isolated legacy verifier, then atomically replace the legacy credential after a successful check. Never write new MD5 values:

javascript
async function migrateHash(password, user, costFactor) {
  const valid = await verifyLegacyMd5ForUser(password, user.legacyHash);
  if (!valid) {
    return false;
  }
  const newHash = await bcrypt.hash(password, costFactor);
  await replaceCredentialInTransaction(user.id, newHash, {
    removeLegacyHash: true
  });
  return true;
}

The legacy verifier must be rate-limited and removed after the migration window; verifyLegacyMd5ForUser is an application boundary, not a recommendation to implement MD5 password storage.

Primary Sources

Conclusion

Bcrypt remains a practical compatibility choice for password hashing when a maintained implementation, an explicitly tested input policy, and a calibrated cost are used. It is not the default answer for every new system: current OWASP guidance prefers Argon2id when available.

Key points to remember:

  • Use a cost factor calibrated to the deployment's hardware and concurrency
  • The hash string contains everything needed for verification
  • Upgrade cost factor as hardware improves
  • Consider Argon2 for new projects with memory-hard requirements

Do not submit real passwords to an online service. Use a local test fixture or an approved, isolated environment when validating an implementation.