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 against a slow hash, which is the whole point of using one. 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. For the full set of safe options, dashboard, WP-CLI, and emergency database access, see how to change a WordPress password. 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
Since WordPress 6.8 (April 2025), new user passwords are hashed with bcrypt via PHP's native password_hash(), with a SHA-384 pre-hash and a $wp$2y$ prefix. Before 6.8, from version 2.5 onward, WordPress used the phpass portable hash (prefix $P$), an MD5-based hash that is salted and iterated 256 times.
Old $P$ hashes still verify after the update and are upgraded to bcrypt on the user's next successful login.
No, and this is the most common misconception. The pre-6.8 phpass portable hash is MD5-based, but it is salted with a per-password random salt and stretched over 256 iterations, so it is dramatically stronger than a bare MD5(password). Since WordPress 6.8 the algorithm is bcrypt, which is not MD5 at all. WordPress has not stored plain unsalted MD5 by default since version 2.5 in 2008.
The user_pass column on the wp_users table. It is a VARCHAR(255), and it holds the one-way password hash (phpass or bcrypt), never the plaintext. The same column has held both formats without a schema change, because VARCHAR(255) is wide enough for any of them.
You can, but use a real hash, not a raw MD5. Historically UPDATE wp_users SET user_pass = MD5('newpass') worked because WordPress detected a bare MD5 and upgraded it on next login, but that is a legacy compatibility path and a security liability. Prefer WP-CLI (wp user update <id> --user_pass='...') or, if you must use SQL, generate a proper bcrypt hash with wp_hash_password() first and store that.
Yes, in WordPress 6.8, released April 2025. wp_hash_password() and wp_check_password() now use PHP's password_hash() / password_verify() with bcrypt and a SHA-384 pre-hash (to get past bcrypt's 72-byte input limit). New hashes carry a $wp$2y$ prefix, and existing $P$ phpass hashes are migrated to bcrypt automatically the next time each user logs in.
bcrypt ignores any input past 72 bytes, so a long passphrase would be silently truncated. Running the password through SHA-384 first turns any length into a fixed-size digest (base64-encoded before bcrypt sees it), so the entire password contributes to the final hash. The $wp prefix marks these WordPress pre-hashed hashes so they are not confused with a plain $2y$ bcrypt hash written by a plugin.
It cannot be decrypted, a one-way hash has no inverse. It can in principle be cracked by guessing: an attacker runs candidate passwords through the same function and looks for a match. The work that takes depends on the algorithm. A bcrypt $wp$2y$ hash (WordPress 6.8 and later) is slow by design, so guessing is expensive and a strong, unique password stays out of reach. A legacy phpass $P$ hash is MD5-based and faster to attack, but still salted and iterated, so it is far from trivial, not the billions-per-second a bare MD5() allows. Either way the defence is the same: long, unique passwords and a database that does not leak in the first place. See detecting fake WordPress admin users for the related breach-cleanup angle.
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. - How to change a WordPress password for every supported way to reset a login, from the dashboard to WP-CLI to direct SQL.
- Running WordPress 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
- PHP Manual: password_hash() (bcrypt default cost, raised to 12 in PHP 8.4)php.net





