UseToolSuite UseToolSuite

bcrypt vs SHA-256: Password Hashing Compared

Why SHA-256 is wrong for password storage and bcrypt is right. Learn about salting, key stretching, and modern password hashing best practices with real benchmarks and implementation examples.

Necmeddin Cunedioglu Necmeddin Cunedioglu 10 min read
Part of the Web Security: Encoding and Hashing Guide series

Practice what you learn

Hash Generator

Try it free →

bcrypt vs SHA-256: Password Hashing Compared

If you’re building authentication, choosing the right hashing algorithm is one of the most important security decisions you’ll make. A poor choice can expose every user’s password within hours of a database breach. This guide provides a deep, technical comparison of SHA-256 and bcrypt, explains why general-purpose hash functions are categorically wrong for password storage, and covers modern alternatives like Argon2id and scrypt.

The Fundamental Problem: Speed Is the Enemy

The core issue is deceptively simple. SHA-256 was designed to be fast — as fast as possible. A modern NVIDIA RTX 4090 GPU can compute over 22 billion SHA-256 hashes per second. This extreme speed is exactly what you want for data integrity verification and digital signatures, but it is catastrophic for password storage.

AlgorithmHashes/sec (RTX 4090)Time to Brute-Force 8-char PasswordTime to Brute-Force 12-char Password
MD5~164 billionSecondsHours to days
SHA-1~27 billionMinutesDays to weeks
SHA-256~22 billionMinutes to hoursWeeks to months
bcrypt (cost 10)~32 thousandDecadesHeat death of the universe
bcrypt (cost 12)~8 thousandCenturiesHeat death of the universe
Argon2id (default)~1 thousandMillenniaHeat death of the universe

The speed difference is not incremental — it is six orders of magnitude. bcrypt is intentionally, deliberately slow, and that slowness is its greatest security feature.

Understanding Cryptographic Hash Functions vs. Key Derivation Functions

Before comparing specific algorithms, it is critical to understand the distinction between two fundamentally different categories of functions.

Cryptographic Hash Functions (SHA-256, SHA-512, SHA-3)

A cryptographic hash function takes an input of any size and produces a fixed-size output (digest). Its design goals are:

PropertyMeaningWhy It Matters
DeterministicSame input always produces the same outputRequired for verification
Fast computationHash can be computed quickly for any inputNeeded for file integrity, blockchain, TLS
Pre-image resistanceCannot reverse the hash to find the inputOne-way function security
Collision resistanceExtremely unlikely for two inputs to produce the same hashPrevents forged certificates
Avalanche effectTiny input change produces completely different hashPrevents pattern analysis

SHA-256 excels at all of these goals. It is the backbone of TLS certificate chains, Git commit verification, Bitcoin proof-of-work, and file integrity checking. But none of these design goals include resistance to brute-force attacks on short, human-chosen inputs like passwords.

Key Derivation Functions (bcrypt, scrypt, Argon2)

A Key Derivation Function (KDF) is specifically designed to derive a cryptographic key from a low-entropy input (like a human password). Its design goals are fundamentally different:

PropertyMeaningWhy It Matters
Intentionally slowEach computation takes a controllable amount of timeThrottles brute-force attacks
Built-in saltRandom value mixed into each hashPrevents rainbow table attacks
Configurable costWork factor can be increased as hardware improvesFuture-proofs against Moore’s Law
Memory-hard (Argon2, scrypt)Requires large amounts of RAM per computationResists GPU/ASIC parallelization

Why SHA-256 Fails for Password Storage

Problem 1: Devastating Speed

SHA-256("password123") → ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f

An attacker who obtains this hash from a database breach can use a tool like Hashcat to try billions of candidate passwords per second. The math is straightforward:

  • A typical 8-character password using lowercase, uppercase, digits, and symbols has approximately 95^8 ≈ 6.6 quadrillion possible combinations
  • At 22 billion SHA-256 hashes per second, exhaustive search takes approximately 3.5 days
  • With dictionary attacks and rule-based mutations, most real-world passwords fall in minutes to hours

Problem 2: No Built-in Salt

Without a salt, identical passwords always produce identical hashes:

User Alice: SHA-256("password123") → ef92b778bafe...
User Bob:   SHA-256("password123") → ef92b778bafe...  (identical!)

This creates two devastating attack vectors:

Rainbow table attacks: An attacker precomputes hashes for millions of common passwords and stores them in a lookup table. When they breach a database, they simply look up each hash. A rainbow table for all 8-character passwords with SHA-256 can be stored in a few terabytes.

Cross-user analysis: If an attacker sees that 500 users have the same hash, they know all 500 users share the same password. Cracking one cracks all of them.

You can add a salt manually before hashing with SHA-256, but this is error-prone. Developers frequently make mistakes like using a static salt for all users, using too-short salts, or storing the salt in a separate location.

Problem 3: No Adaptive Cost Factor

SHA-256 always performs the same computation — there is no way to increase the work required per hash. As GPUs get faster each year, SHA-256 password cracking gets proportionally faster. An algorithm that takes minutes to crack today will take seconds tomorrow.

Generate SHA-256 hashes for data integrity verification (not passwords!) with our Hash Generator.

Why bcrypt Is the Right Choice

bcrypt was designed in 1999 by Niels Provos and David Mazières, specifically for password hashing. It is based on the Blowfish cipher and solves all three problems that SHA-256 has.

Built-in Salt Generation

Every bcrypt hash includes a unique, randomly generated 128-bit salt:

bcrypt("password123", cost=12)
  → $2b$12$LJ3m4ys3Lg5Ey1j3EXAMPLE.HashedOutputHere..............
       │  │  └── 22-character salt (Base64-encoded)
       │  └── Cost factor (2^12 = 4096 iterations)
       └── Algorithm identifier ($2b = bcrypt)

Two users with the same password “password123” will always get different hashes because each call generates a new random salt. This makes rainbow tables completely useless — an attacker would need to build a separate rainbow table for every possible salt value.

Configurable Cost Factor (Key Stretching)

The cost factor (also called work factor or rounds) controls how many iterations of key expansion bcrypt performs internally. Each increment doubles the computation time:

Cost FactorInternal IterationsApproximate Time per HashRecommended For
416~1msTesting/development only
8256~15msNot recommended for production
101,024~65msMinimum for production
124,096~250msRecommended for most applications
1416,384~1,000msHigh-security applications
1665,536~4,000msExtremely sensitive systems

The key insight: a 250ms delay is imperceptible during login for a legitimate user. But for an attacker trying to guess billions of passwords, that 250ms per attempt makes brute-force computationally infeasible.

Recommendation: Start with cost 12 and benchmark on your production hardware. The hash should take between 200-500ms. As hardware improves over the years, increase the cost factor by one level.

Resistance to GPU and ASIC Attacks

bcrypt’s key expansion uses the Blowfish cipher, which requires frequent memory accesses to a 4KB state table (the Blowfish S-boxes). GPU architectures are optimized for massively parallel, simple arithmetic — not for memory-intensive operations with data-dependent access patterns. This makes bcrypt approximately 1000x harder to parallelize on GPUs compared to SHA-256.

Modern Alternatives: Argon2id and scrypt

While bcrypt remains excellent, two newer algorithms offer additional protections:

FeaturebcryptscryptArgon2id
Year introduced199920092015
CPU hardness✅ Yes✅ Yes✅ Yes
Memory hardness❌ No (fixed 4KB)✅ Yes (configurable)✅ Yes (configurable)
Parallelism resistance⚠️ Moderate✅ Good✅ Excellent
GPU/ASIC resistance⚠️ Moderate✅ Good✅ Excellent
Side-channel resistance⚠️ Moderate⚠️ Moderate✅ Excellent (hybrid mode)
Library support✅ Universal✅ Good✅ Growing rapidly
Standards body recommendationIndustry standardNIST mentionedOWASP recommended

Argon2id is the winner of the 2015 Password Hashing Competition and is the recommended choice for new projects. It combines:

  • Argon2d: Data-dependent memory access (GPU/ASIC resistant but vulnerable to side-channel attacks)
  • Argon2i: Data-independent memory access (side-channel resistant but less GPU resistant)
  • Argon2id: Hybrid approach — first pass uses data-independent access, subsequent passes use data-dependent access

scrypt is a good intermediate choice when Argon2 libraries are unavailable. It was designed by Colin Percival for the Tarsnap backup service and is memory-hard, but its parameter tuning is less intuitive than Argon2.

Implementation Examples

Node.js with bcrypt

import bcrypt from 'bcrypt';

const COST_FACTOR = 12;

// Hash a password during registration
async function hashPassword(plaintext) {
  const hash = await bcrypt.hash(plaintext, COST_FACTOR);
  return hash; // Store this in your database
}

// Verify a password during login
async function verifyPassword(plaintext, storedHash) {
  const isValid = await bcrypt.compare(plaintext, storedHash);
  return isValid; // true if password matches
}

// Example usage
const hash = await hashPassword('MySecureP@ssw0rd!');
console.log(hash);
// $2b$12$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa

const isCorrect = await verifyPassword('MySecureP@ssw0rd!', hash);
console.log(isCorrect); // true

Python with bcrypt

import bcrypt

COST_FACTOR = 12

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

# Verify a password
def verify_password(plaintext: str, stored_hash: bytes) -> bool:
    return bcrypt.checkpw(plaintext.encode('utf-8'), stored_hash)

# Example
hashed = hash_password('MySecureP@ssw0rd!')
print(verify_password('MySecureP@ssw0rd!', hashed))  # True

Go with bcrypt

package main

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

const cost = 12

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

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

func main() {
    hash, _ := hashPassword("MySecureP@ssw0rd!")
    fmt.Println("Hash:", hash)
    fmt.Println("Valid:", verifyPassword("MySecureP@ssw0rd!", hash))
}
import argon2 from 'argon2';

// Hash with Argon2id (recommended variant)
async function hashPassword(plaintext) {
  const hash = await argon2.hash(plaintext, {
    type: argon2.argon2id,
    memoryCost: 65536,    // 64 MB of RAM
    timeCost: 3,           // 3 iterations
    parallelism: 4,        // 4 parallel threads
  });
  return hash;
}

// Verify
async function verifyPassword(plaintext, storedHash) {
  return await argon2.verify(storedHash, plaintext);
}

Best Practices for Password Hashing in Production

PracticeDetailsPriority
Never use MD5, SHA-1, or SHA-256 for passwordsThey are far too fast and lack built-in saltingCritical
Use bcrypt with cost 12+ or Argon2idBalance security with acceptable login latencyCritical
Generate strong passwordsUse our Password Generator to create high-entropy credentialsHigh
Rehash on login when upgrading costWhen you increase the cost factor, hash passwords again as users log inHigh
Never store plaintext passwordsObvious, yet database breaches still expose plaintext credentialsCritical
Never log passwordsEnsure passwords are never written to application logs, error traces, or monitoring systemsHigh
Implement rate limitingLimit login attempts to prevent online brute-force attacks (e.g., 5 attempts per minute)High
Use constant-time comparisonPrevent timing attacks when comparing hashes (bcrypt.compare does this automatically)Medium
Pepper your hashesAdd a server-side secret (pepper) before hashing for defense-in-depthMedium

The Rehashing Strategy

When you upgrade your cost factor (e.g., from 10 to 12), you cannot rehash existing passwords because you don’t have the plaintext. The solution is to rehash during login:

async function loginUser(email, password) {
  const user = await db.findUserByEmail(email);
  if (!user) return null;

  const isValid = await bcrypt.compare(password, user.passwordHash);
  if (!isValid) return null;

  // Check if the hash needs upgrading
  const currentCost = parseInt(user.passwordHash.split('$')[2]);
  if (currentCost < TARGET_COST) {
    const newHash = await bcrypt.hash(password, TARGET_COST);
    await db.updatePasswordHash(user.id, newHash);
  }

  return user;
}

Common Misconceptions Debunked

MisconceptionReality
”SHA-256 is more secure because it’s newer”SHA-256 is a better hash function, but being “better” at general hashing makes it worse for passwords — it’s faster
”Adding salt to SHA-256 makes it safe for passwords”Salt prevents rainbow tables but does nothing against brute-force speed. You still get billions of attempts per second
”bcrypt is too slow for my application”250ms per login is imperceptible to users. If your application hashes passwords more than a few times per second, your architecture has other issues
”I can use SHA-256 with many rounds (like PBKDF2)“PBKDF2-SHA-256 with 600,000+ iterations is acceptable per OWASP, but Argon2id and bcrypt are still preferred because they resist GPU parallelization
”Password hashing doesn’t matter if my database is secure”Defense in depth — databases get breached. Every year, major companies lose user data. Proper hashing is your last line of defense

Further Reading


This article is part of our Encoding and Hashing Guide series. Generate secure passwords with our Password Generator, create bcrypt hashes with our Bcrypt Generator, and compute SHA-256 checksums with our Hash Generator.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author
10 min read
-- views

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.