10LOC
Bunintermediate

Password hashing with Bun.password, no bcrypt dependency

Published November 13, 2026

export const hashPassword = (password: string) =>
  Bun.password.hash(password, { algorithm: "argon2id", memoryCost: 19456, timeCost: 2 });

export const verifyPassword = (password: string, hash: string) => Bun.password.verify(password, hash);

const hash = await hashPassword("correct horse battery staple");
console.log(await verifyPassword("correct horse battery staple", hash)); // true
console.log(await verifyPassword("wrong guess", hash)); // false

What

Bun.password.hash(password, options) hashes a password with argon2 (default) or bcrypt and returns a self-describing string with the algorithm, parameters, and salt baked in. Bun.password.verify(password, hash) reads that string back and checks a candidate password against it.

Why it matters

Hashing passwords in Node almost always means installing bcrypt (native bindings, a postinstall build step) or bcryptjs (pure JS, slower). Bun implements both argon2 and bcrypt natively in the runtime, so Bun.password needs no dependency and no native compile step. Argon2 is also the more modern default — it won the Password Hashing Competition and is generally recommended over bcrypt for new code — and Bun defaults to it while still supporting bcrypt for compatibility with existing hashes from another system.

How it works

  • hashPassword sets algorithm: "argon2id" explicitly (argon2id is also the library default) along with memoryCost and timeCost, the two knobs that trade hashing time for resistance to GPU-accelerated cracking attempts.
  • The salt is generated automatically and stored inside the returned hash string — there's no separate salt to manage or persist yourself.
  • verifyPassword doesn't need to know which algorithm or parameters produced the hash: verify parses that out of the hash string itself, so changing hashPassword's settings later doesn't invalidate the ability to verify old hashes.
  • Both functions are async by default because hashing is deliberately slow (that's the whole point); Bun.password.hashSync/verifySync exist too, but block the event loop for the duration of the hash.

Gotchas

  • Never compare passwords or hashes with === — always go through Bun.password.verify, which is written to run in constant time regardless of where the strings first differ.
  • Higher memoryCost/timeCost (argon2) or cost (bcrypt) means slower login for legitimate users too — pick values based on your server's actual hardware, not just security tables written for a specific reference machine.
  • bcrypt silently truncates passwords over 72 bytes in most implementations; Bun's bcrypt mode avoids that specific footgun by hashing longer inputs with SHA-512 first, but it's still worth knowing bcrypt has that limit if you compare hashes across systems.

Related

  • Bun.CryptoHasher — general-purpose cryptographic hashing (SHA-256, etc.) for data integrity, not passwords; it has no built-in salting or slow-hashing and shouldn't be used for credentials.
  • Session tokens after login are a separate concern from password hashing — Bun.password only covers verifying the credential itself.