WordPress never stores your password. It stores a one-way hash of it, in a single column: user_pass on the wp_users table, which is a VARCHAR(255). Feed a login attempt back through the same hashing function, compare the result against what is stored, and that is how it checks you in without ever knowing the plaintext.
Short answer: the hash lives in wp_users.user_pass, a VARCHAR(255) column. From WordPress 2.5 (2008) through 6.7, that hash was a phpass portable hash with a $P$ prefix, an MD5-based but deliberately iterated hash, not raw MD5. Since WordPress 6.8 (April 2025), new hashes are bcrypt (with a $wp$2y$ prefix and a SHA-384 pre-hash step), and old $P$ hashes are transparently upgraded to bcrypt the next time the user logs in. The column stayed VARCHAR(255) the whole way through, which is exactly why that width is the right default for any password column you build yourself.
I ran a WordPress agency for years, and the number of times I had to explain to a client that "we genuinely cannot tell you your old password, only reset it" is too high to count. This is why.
Where WordPress keeps the password
There is no separate credentials store, no encrypted vault, no second table. The hash sits in the same wp_users row as your login name and email:
SELECT ID, user_login, user_pass FROM wp_users WHERE user_login = 'admin';
-- ID user_login user_pass
-- 1 admin $P$BVbR9X8a4Q1f0sH4nLpQpC3mDxkqU./The schema declares user_pass as a VARCHAR(255):
CREATE TABLE wp_users (
ID BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_login VARCHAR(60) NOT NULL DEFAULT '',
user_pass VARCHAR(255) NOT NULL DEFAULT '',
user_nicename VARCHAR(50) NOT NULL DEFAULT '',
user_email VARCHAR(100) NOT NULL DEFAULT '',
-- ... user_url, user_registered, user_activation_key, etc.
PRIMARY KEY (ID),
KEY user_login_key (user_login),
KEY user_nicename (user_nicename),
KEY user_email (user_email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Two things matter here. First, the value is never the plaintext and never reversible. A hash function only goes one way, so a stolen wp_users table does not hand the attacker your password. It hands them a value they have to brute-force, which is the whole point of using a slow hash. Second, the column is VARCHAR(255), comfortably wider than any hash WordPress has ever put in it. A phpass portable hash is 34 characters; a WordPress bcrypt hash is around 60-65. The 255 leaves enormous headroom, which is deliberate, and I will come back to why it is the right call for your own tables.
The phpass era: $P$ portable hashes
From WordPress 2.5 in 2008 until 6.7, WordPress hashed passwords with phpass, the Portable PHP Password Hashing framework written by Solar Designer (Alexander Peslyak). The implementation lives in wp-includes/class-phpass.php as the PasswordHash class, and WordPress configures it to use phpass's "portable hash" mode.
A portable hash looks like this:
$P$BVbR9X8a4Q1f0sH4nLpQpC3mDxkqU./
Breaking that apart:
$P$is the identifier for a phpass portable hash. (WordPress core's variant sometimes emits$H$, the same format under a different tag.)- The next character (
Babove) encodes the iteration count as a power of two. WordPress instantiates the hasher withiteration_count_log2of 8, so the hash is computed with2^8 = 256passes of MD5. - The following 8 characters are the per-password random salt.
- The remainder is the base64-style encoded digest.
The single most common misconception, and one I corrected on the IRC #wordpress channel more times than I can remember, is "WordPress passwords are just MD5". They are not. The portable hash is MD5-based: the underlying primitive is MD5, but it is salted and stretched. The salt (unique per password) defeats rainbow tables, and the 256-pass loop makes each guess hundreds of times more expensive than a single bare MD5. That is night-and-day against a plain MD5(password), which a GPU chews through at billions of guesses per second.
It is still not great by 2025 standards. MD5 is fast even iterated a few hundred times, and modern cracking rigs make portable hashes meaningfully weaker than a proper slow hash. That is exactly why the project moved on. But "WordPress stores your password as MD5" was always wrong, and worth correcting, because it leads people to assume a leaked wp_users table is trivially crackable. It is not as trivial as raw MD5, though for why raw MD5 is the wrong choice for passwords, the reasoning is the same family of problem.
The public API, all defined in wp-includes/pluggable.php so plugins can override them, is:
wp_hash_password($password)produces the stored hash.wp_check_password($password, $hash, $user_id)verifies a login attempt against the stored hash.wp_set_password($password, $user_id)hashes and writes the new value intouser_passdirectly.
Because they are pluggable, security plugins (and Roots' old wp-password-bcrypt drop-in) could swap the algorithm out long before core did.
The 2025 switch to bcrypt (Updated)
Updated for WordPress 6.8, April 2025. WordPress 6.8 changed the default algorithm for new password hashes from the phpass portable hash to bcrypt, closing a Trac ticket that had been open since 2012. I verified the specifics below against the official Make WordPress Core announcement.
Here is what actually changed for the user password in user_pass:
wp_hash_password()andwp_check_password()now use PHP's nativepassword_hash()andpassword_verify()with the bcrypt algorithm. No more phpass for new hashes.- Passwords are pre-hashed with a keyed SHA-384 (HMAC) before bcrypt. bcrypt silently truncates its input at 72 bytes, so a long passphrase would lose everything past byte 72. WordPress runs the password through HMAC-SHA-384 (keyed with the literal string
wp-sha384for domain separation) first, collapsing any length into a fixed-size digest so the full password contributes to the result. (The digest is base64-encoded before being handed to bcrypt so it is safe to pass as a string.) - New hashes carry a
$wp$prefix, so the full stored value starts with$wp$2y$. The$wpmarker disambiguates a WordPress SHA-384-pre-hashed bcrypt hash from a plain$2y$bcrypt hash that a plugin might have written. It is not used for any algorithm other than bcrypt.
A stored 6.8 hash looks roughly like:
$wp$2y$10$Q9... (bcrypt, cost 10, SHA-384 pre-hashed, WordPress-tagged)
The cost factor (10 above) is PHP's password_hash() default, not a hardcoded WordPress value: it is 10 on PHP up through 8.3 and 12 on PHP 8.4. WordPress deliberately leaves it to the PHP default so the work factor rises for free as PHP raises it, with no WordPress release needed. The cost is encoded in the hash, so an old cost-10 hash keeps verifying after PHP bumps the default.
Crucially, the upgrade is transparent and lazy. Old $P$ portable hashes are not invalidated by the update. wp_check_password() still recognises and verifies them, and when a user with a legacy hash logs in successfully (or resets their password), WordPress rehashes the now-known plaintext with bcrypt and rewrites user_pass in place. So a site that updates to 6.8 migrates its users to bcrypt gradually, one login at a time, with zero forced resets.
One thing to keep separate: WordPress 6.8 also moved application passwords, password-reset keys, personal-data-request keys, and the recovery-mode key off phpass, but those use a fast BLAKE2b hash via Sodium (wp_fast_hash() / wp_verify_fast_hash(), with a $generic$ prefix), not bcrypt. That is correct: those values are already long and random, so they do not need a slow hash. The user login password is the one that gets bcrypt. Do not confuse either of these with the AUTH_KEY / SECURE_AUTH_KEY salts in wp-config.php either. Those are cookie-signing secrets, not password hashes, and they never touch user_pass.
Checking and setting a password the WordPress way
If you are writing code against WordPress, never touch user_pass with raw SQL. Use the API and let it pick the current algorithm:
// Verify a login attempt
if ( wp_check_password( $entered, $user->user_pass, $user->ID ) ) {
// correct; wp_check_password also triggers the bcrypt
// upgrade for legacy $P$ hashes on success
}
// Set a new password (hashes + writes user_pass for you)
wp_set_password( $new_plaintext, $user->ID );wp_set_password() runs the plaintext through wp_hash_password() and updates the row, so on 6.8+ you get a bcrypt $wp$2y$ hash automatically, and you never assemble the hash yourself.
The SQL gotcha. There is an old trick, ten-plus years of forum answers deep, of resetting a locked-out admin straight in the database with UPDATE wp_users SET user_pass = MD5('newpass') WHERE .... This historically worked because WordPress's login path detected a bare 32-character MD5 in user_pass, accepted it once, and immediately upgraded it to a proper salted hash on that first successful login. It was a deliberate backward-compatibility path from the pre-2.5 days when WordPress really did store plain MD5.
I would not lean on it now. It is a legacy code path, it is exactly the foothold an attacker with write access to your database wants, and it is the kind of thing that quietly gets tightened in a future release. The clean way to reset a locked-out user is wp_set_password() (via WP-CLI: wp user update <id> --user_pass='...'), which writes a current-algorithm hash directly. If you must go through SQL, generate a real bcrypt hash in PHP with wp_hash_password() and paste that into user_pass, rather than dropping a raw MD5 and relying on the upgrade path.
What this means if you store passwords yourself
The WordPress story is a clean lesson for anyone designing their own auth table, and it comes down to two decisions.
Use a slow, purpose-built password hash at the application layer. bcrypt, argon2id, or scrypt. Never a bare MD5() or SHA2() in the database. WordPress spent over a decade on an iterated-MD5 hash precisely because a fast hash is a liability for passwords: speed helps the attacker brute-forcing a stolen table, not you. The move to bcrypt in 6.8 is the project finally landing where new code should have started.
Store the result in a VARCHAR(255). This is the part people get wrong by under-sizing. A bcrypt hash is 60 characters, argon2id output runs longer, and the encoded string already carries the algorithm, cost factor, and salt inside it, so you store the one value and nothing else. VARCHAR(255) is the right width for a password column because it covers every modern scheme with room to spare and costs nothing for shorter values (a VARCHAR only uses the bytes it needs plus a length prefix). WordPress kept user_pass at VARCHAR(255) across both the phpass and bcrypt eras without a schema change, which is the whole argument: size the column for the format, not for today's algorithm. If you want the dedicated walkthrough, see how to store a bcrypt hash in MySQL and the wider question of which column type a password belongs in, backed by the byte sizes in the MySQL data types reference.
FAQ
See also
- Storing a bcrypt password hash in MySQL walks through the exact column setup WordPress lands on in 6.8.
- Which column type a password belongs in covers why
VARCHAR(255)is the safe default for any auth table. - Storing an argon2 password hash in MySQL for the stronger alternative to bcrypt when your stack supports it.
- Storing a SHA-256 hash in MySQL for the non-password case where a fast hash is actually the right tool.
- The MySQL data types reference for the byte sizes behind the
VARCHARsizing argument. - Running MySQL in Docker if you want a throwaway instance to inspect a real
wp_userstable.
Sources
Authoritative references this article was fact-checked against.
- Make WordPress Core: WordPress 6.8 will use bcrypt for password hashingmake.wordpress.org
- WordPress Developer Reference: wp_hash_password()developer.wordpress.org
- WordPress Developer Reference: wp_check_password()developer.wordpress.org
- Openwall: Portable PHP password hashing (phpass) frameworkopenwall.com
- WordPress core: wp-includes/class-phpass.php (PasswordHash class)github.com
- WordPress core PR #7333: Switch to using bcrypt for hashing passwordsgithub.com





