Bcrypt has been hashing passwords since 1999 and is still a defensible default. What the salt in every hash actually defends against, why slowness is the feature and not the bug, how to read the 60-character format, and what cracking one really costs an attacker.
Why passwords need a slow hash
General-purpose hashes are built for speed, and speed is fatal here: a single GPU pushes tens of billions of MD5 or SHA-256 guesses per second, enough to walk through every eight-character password in hours. Password hashing turns that property upside down. Bcrypt runs an expensive key setup 2^cost times, so each guess costs real work, tuned by you: cost 12 means 4,096 rounds and tens of milliseconds per attempt. Your login pays that price once per user; an attacker pays it once per guess, billions of times. The TIME stat in the tool above measures it live, which makes the trade concrete in a way no explanation does.
This is also why "just use SHA-256 with a salt" fails the audit: the salt stops precomputation, but a salted fast hash still yields billions of guesses per second. Slow and salted is the pair; either alone loses. Choosing between bcrypt, scrypt and Argon2, cost tuning, peppers and migration of old hashes is its own topic, covered in our password hashing guide, which also walks through the OWASP recommendations the field treats as the reference.
What a salt is (and what it is not)
A salt is random bytes, unique per password, stored in plain sight next to the hash. It has exactly two jobs. First, it makes equal passwords unequal in the database: without salts, every user who picked summer2026 shares one hash, so cracking it once opens all their accounts, and the duplicate rows alone leak which passwords are popular. Second, it destroys precomputation: the lookup tables and rainbow tables that "reverse" unsalted MD5 in milliseconds would have to be rebuilt per salt, per account, which converts a one-time global investment into a per-target brute force.
Just as important is what a salt is not. It is not secret: it sits in the hash string, and hiding it buys nothing, because the threat model assumes the database already leaked. It does not slow down an attack on a single account; that is the cost factor's job. And it is not a key: a secret mixed into hashing is a pepper, best applied as an HMAC with a key stored outside the database. Salts defeat scale, cost defeats speed, peppers hedge the database leak; three mechanisms, three different failures they answer.
Bcrypt's quiet design win is making the salt impossible to forget: the library generates 128 random bits per hash and writes them into the output string itself. There is no "salt column", no way to reuse one by accident, and verification reads it back out transparently. Hash the same password twice in the tool above and watch two different, equally valid strings appear; that is the salt doing its first job.
Anatomy of a bcrypt hash
| Piece | Example | Meaning |
|---|---|---|
| Prefix | $2b$ | algorithm revision; $2a$ and PHP's $2y$ are the same math |
| Cost | 12 | 2^12 = 4,096 rounds of key expansion, doubling per step |
| Salt | R9h/cIPz0gi.URNNX3kh2O | 22 characters of bcrypt's own Base64 = 128 random bits |
| Digest | PST9/PgBkqquzi.Ss7KIUgO… | 31 characters encoding the 23-byte Blowfish result |
Everything a verifier needs travels inside the string, which is why bcrypt columns are a single CHAR(60) and why the checker on this page can re-run a pasted hash without asking you for parameters. Two format quirks are worth knowing: the Base64 alphabet is bcrypt's own (./A–Za–z0–9, not RFC 4648), so standard decoders reject it, and the 72-byte input limit means bytes beyond that are silently ignored, a real concern for passphrase users and the reason the tool warns instead of staying quiet.
How to use this tool
- Hash: type a password, pick a cost, read the result with its anatomy spelled out underneath. The hash changes on every run; that is the fresh salt, not a bug.
- Check: paste an existing hash into pane 02 and the CHECK stat says whether the password produces it. The stored cost and salt are read from the pasted hash itself, so the cost buttons only affect the fresh hash above.
- Read the failures: a paste that is not bcrypt gets named, whether it is Argon2, sha512crypt from a Linux shadow file, or a hash truncated by a too-short database column, the second most common bcrypt bug we have met in the wild.
What cracking bcrypt costs
| Hash | Guesses/s on one RTX 4090 | 8 random chars (lower+digits) |
|---|---|---|
| MD5 | ~160 billion | ~20 minutes |
| SHA-256 | ~20 billion | ~2.5 hours |
| bcrypt, cost 10 | ~180 thousand | ~500 years |
| bcrypt, cost 12 | ~45 thousand | ~2,000 years |
The table (hashcat benchmark figures, rounded) is the whole argument in four rows: the same GPU that eats fast hashes for breakfast slows to a crawl against bcrypt, and every cost step doubles the crawl. It also shows the limit of the protection: those 500 years assume a random password. A dictionary word with a digit stapled on sits in the first few million guesses and falls in under a minute even at cost 12. The hash buys time against guessing; only the password's actual randomness decides how much that time is worth, which is what our password generator is honest about on the other end.
bcrypt questions
What is a salt, and why do password hashes need one?
A salt is a random value, typically 16 bytes, generated per password and stored in the open right next to the hash. It fixes two failures of unsalted hashing: identical passwords no longer produce identical hashes, so an attacker cannot spot shared passwords or crack one hash and unlock every account using it; and precomputed lookup or rainbow tables become useless, because a table would have to be built per salt. A salt is not secret and does not slow an attacker targeting a single hash; that is the job of the cost factor. Bcrypt bakes the salt into the output string, which is why no separate salt column exists in a bcrypt-based schema.
Why does bcrypt produce a different hash for the same password every time?
Because every call generates a fresh random salt, and the salt is part of the output. $2b$10$N9qo8uLO… and $2b$10$R9h/cIPz… can both be correct hashes of the same password. This is by design and does not break verification: a bcrypt check does not hash the password and compare strings, it reads the salt and cost out of the stored hash, re-runs bcrypt with exactly those, and compares the result. That is also why functions like compareSync(password, storedHash) take the whole stored string, salt included, as their second argument.
What is the difference between $2a$, $2b$ and $2y$ bcrypt prefixes?
They are revision markers of the same algorithm, not different algorithms. $2a$ is the 1997 original marker (with a later-discovered sign-extension bug in some implementations), $2y$ is PHP's marker introduced to disambiguate from that bug era, and $2b$ is the current OpenBSD revision that fixed a wraparound quirk; $2x$ marks deliberately bug-compatible hashes. For verification they are interchangeable in every maintained library, and all describe hashes of the shape $prefix$cost$22-char-salt31-char-digest. You never need to migrate hashes between prefixes; new hashes simply come out as $2b$ (or $2y$ from PHP).
Why does a bcrypt hash generated in PHP fail to verify in Node.js or Python?
Usually one of three things, in this order. The $2y$ prefix: PHP writes it, and while bcryptjs and passlib accept it, some libraries want it rewritten to $2a$/$2b$ first (a plain string replace is safe, the math is identical). Second, the hash was mangled in transit: a VARCHAR(50) column truncating the 60 characters, or an environment variable eating a $ sign, are classics. Third, the password bytes differ, typically UTF-8 versus a legacy encoding for non-ASCII characters, or a trailing newline read from stdin. The hash format itself is portable; verify with the same bytes and any maintained library agrees.
Can you decrypt a bcrypt hash?
No. Bcrypt is a one-way key derivation, not encryption: nothing in the 60-character string contains the password in recoverable form, and the only way back is guessing. What attackers actually do is run candidate passwords through bcrypt with the hash's own salt and cost until one matches. At cost 12 a single modern GPU manages only tens of thousands of guesses per second (against tens of billions for MD5), so a random 12-character password holds for geological timescales. A top-100 password still falls in under a second, which is why the hash function can never compensate for a guessable password.
How do I verify a bcrypt hash in Node.js, Python or PHP?
Node.js: (await import("bcryptjs")).compare(password, hash) or the native bcrypt package's bcrypt.compare; both return a promise of true/false. Python: bcrypt.checkpw(password.encode(), hash.encode()) from the bcrypt package, or passlib's bcrypt.verify. PHP: password_verify($password, $hash), which handles $2y$ natively. All of them parse cost and salt out of the stored string, so no salt handling appears in application code. Never compare hash strings with equality yourself: a fresh hash of the same password is a different string by design.
Is it safe to type a real password into an online bcrypt generator?
Treat it as unsafe by default: most such tools POST what you type to a server, and a password that has touched someone else's server is burned regardless of what the page promises. If you must use one, check the network tab while typing and prefer pages that keep working offline. This tool computes bcrypt in pure JavaScript inside your tab, transmits nothing, and survives flight mode, which you can verify rather than take on faith. For production secrets the cleaner habit is generating test hashes locally: node -e "console.log(require('bcryptjs').hashSync('pw', 12))" does it in one line.