To reset a WordPress password you have four routes, and which one you use is decided by the access you still have: the "Lost your password?" link if email works, the Users screen if you can log in, wp user update if you have a shell, and a direct wp_users write if you only have the database.
Two things that every ranking guide leaves out matter more than the choice. Resetting a password does not end any existing session, so an attacker who is already logged in stays logged in. And since the reset key is hashed in the database, you can no longer build the reset URL by reading a row out of wp_users.
Everything below about core behaviour is from the WordPress core source in wordpress-develop trunk, read on 2026-09-24.
The four methods, ranked by what access you have
You can log in. Users > Profile, then Set New Password. This is the only route that also offers "Log Out Everywhere Else", which matters and is covered below.
Email works, you cannot log in. /wp-login.php?action=lostpassword. Core generates a 20-character key with wp_generate_password( 20, false ), mails you the link, and stores a hash of the key.
You have a shell. WP-CLI is the shortest path:
wp user list --fields=ID,user_login,user_email,roles
wp user update 1 --user_pass='the-new-password'
wp user reset-password 1 --show-password is the other option. It generates the password for you and emails the user by default, so add --skip-email when the mail is not going to arrive anyway (WP-CLI docs, fetched 2026-09-24).
You only have the database. Write to wp_users.user_pass directly. This works, but the mechanism is not what most guides say it is.
What WordPress actually stores in user_pass
Since 6.8, wp_hash_password() uses bcrypt rather than phpass. The hash it writes looks like $wp$2y$10$..., and the $wp prefix is deliberate, added "to facilitate distinguishing vanilla bcrypt hashes" in core's own comment.
Core does not feed your password to bcrypt directly. It runs hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ), base64-encodes that, and hashes the result. The reason is bcrypt's 72-byte input limit: the SHA-384 step keeps the entropy of a longer passphrase instead of silently truncating it.
Two consequences worth knowing. Passwords are trimmed, so a trailing space in your password manager entry is not part of your password. And a password over 4,096 characters makes wp_hash_password() return the literal string *, which no input can ever match.
wp_check_password() reads the prefix to decide how to verify, and it has five branches: a 32-character-or-shorter hash is checked as raw MD5, an overridden $wp_hasher wins if a plugin set one, a $wp-prefixed hash goes through the SHA-384 step, a $P$ hash falls back to phpass, and anything else is passed to plain password_verify().
Why the phpMyAdmin MD5 trick still works
The official WordPress documentation still tells you to paste your password into user_pass with phpMyAdmin's MD5 function selected, and that instruction is correct, for a reason it does not give.
wp_check_password() opens with if ( strlen( $hash ) <= 32 ), and an MD5 hex digest is exactly 32 characters. So core checks any short hash as MD5 regardless of the current algorithm, and that branch has survived the move to bcrypt.
It costs you one thing. On the next successful login, wp_authenticate_username_password() calls wp_password_needs_rehash(), and an unprefixed hash always returns true under bcrypt, so core immediately calls wp_set_password() and replaces your MD5 with a proper bcrypt hash.
That is the honest version of the trick: your password sits in the database as unsalted MD5 until the moment you log in with it. On a live site that is a window you do not want, however short. Use WP-CLI if you have any shell at all.
If you are using a real bcrypt hash generated elsewhere, note that it lands in the last branch, without the $wp prefix and without the SHA-384 step. It authenticates, and it is rehashed on first login for the same reason.
You cannot read the reset link out of the database any more
The common advice for a site whose email is broken is to request a reset, then pull user_activation_key out of wp_users and assemble the URL by hand. That stopped working.
get_password_reset_key() stores time() . ':' . wp_fast_hash( $key ). wp_fast_hash() is a BLAKE2b hash through libsodium with a wp_fast_hash_6.8+ domain separator, so the row holds a timestamp and a one-way hash, and the plaintext key exists only in the email that was sent.
The timestamp half is still useful. check_password_reset_key() splits on the colon and compares against $pass_request_time + $expiration_duration, where the duration is DAY_IN_SECONDS filtered by password_reset_expiration. So you can read whether a key has expired even though you cannot use it:
wp db query "SELECT user_login, user_activation_key FROM wp_users WHERE ID = 1"
A row with no colon in it is a pre-4.3 key. Core deliberately answers those with expired_key rather than invalid_key, so the error message you get distinguishes them.
Why the reset email does not arrive
This is the real reason people end up in the database, so it deserves the specific causes rather than "check your spam folder".
wp_mail() has no transport. A container or VPS with no sendmail binary and no SMTP plugin fails silently, and retrieve_password() reports success to the browser either way.
The address is not what you think it is. Core mails user_email from wp_users, which is not necessarily the address in Settings > General.
Reset is blocked for that user. wp_is_password_reset_allowed_for_user() returns false for a user flagged as spam on multisite, and the allow_password_reset filter lets any plugin refuse, at which point get_password_reset_key() returns a no_password_reset error.
You already used the link. A successful login clears user_activation_key with a direct $wpdb->update(), and so does every call to wp_set_password().
More than a day passed. The key hash stays in the row after expiry, so the link looks intact and fails anyway.
If the mail transport is the problem and you want to see what core would have sent, run the flow on a copy of the site where mail is captured rather than delivered. That is what the Email Log is for: SandyWP intercepts wp_mail() before it leaves, so the reset message and its link land in a log you can read.
What a password reset does not do
This is the finding worth carrying away. wp_set_password() updates exactly two columns, user_pass and user_activation_key, then clears the user cache. It does not touch the session_tokens user meta where WordPress keeps live logins.
reset_password(), which is what the wp-login.php form calls, adds only update_user_meta( $user->ID, 'default_password_nag', false ) on top of that. No session handling there either.
In core, sessions are destroyed only by the "Log Out Everywhere Else" button on the profile screen, which is an Ajax action a person has to click. Nothing in the password path calls wp_destroy_other_sessions().
So if you are resetting a password because the account was compromised, changing it is half the job. The other half:
wp user session destroy 1 --all
wp user application-password list 1
wp user application-password delete 1 --all
Application passwords are the part people forget. They are stored separately, they authenticate REST requests on their own, and a user_pass change has no effect on them at all.
Practise the recovery, not the reset
The reset itself is one command. What goes wrong is everything around it: a database whose table prefix is not wp_, a site with DISALLOW_FILE_MODS set, a multisite where the user you are fixing is on a different subsite, or a plugin filtering allow_password_reset that you do not know about.
Those are worth finding out on a copy. Clone the site with the Cloner, lock yourself out of the copy on purpose, and walk every route until you know which one works on this particular install:
wp user update 1 --user_pass='test-only'
wp option get admin_email
wp eval 'var_dump( wp_is_password_reset_allowed_for_user( 1 ) );'
A sandbox suits this better than staging, because you are deliberately putting the site into a locked-out state and you want to throw it away afterwards. Shell and SFTP access and WP-CLI are both there, and the copy's mail never reaches a real inbox.
When a reset is the wrong fix
If you cannot reach /wp-admin/ at all because a plugin is fatalling, the password is not your problem. Start with recovery mode or the white screen checklist.
If the login form loads, accepts the right password and returns you to the login form, that is a cookie or URL problem, not a credentials one. Check siteurl and home before you reset anything.
And if you inherited a site with no working credentials and no known email address, the database write is legitimate and fast. Just do it through WP-CLI so the hash is right the first time, and destroy the sessions afterwards.
