Change the prefix on a disposable copy of the site first, then check three things: that no option_name or meta_key still starts with the old prefix, that no table was left behind, and that the code still contains no literal wp_. Logging in successfully proves only that the two most famous keys were renamed.
That is the gap in every guide that ranks for this. They all cover the rename, most cover the lockout, and none cover verification.
Why the prefix change locks you out
Almost every article describes the lockout as WordPress "remembering" the old table names. It is the opposite, and the difference tells you what to check.
WordPress builds those key names at runtime, from whatever prefix is currently configured. In WP_Roles the option name is built as $this->role_key = $wpdb->get_blog_prefix( $this->site_id ) . 'user_roles';.
WP_User does the same thing for capabilities: $this->cap_key = $wpdb->get_blog_prefix( $this->site_id ) . 'capabilities';.
So the moment you set $table_prefix to xk3_, WordPress starts looking for an option called xk3_user_roles and a user meta key called xk3_capabilities. Neither exists. It finds no roles at all, so every capability check fails and every user is effectively nobody.
This is why the fix is a rename of the keys and not a repair of anything. And it is why the real list of things to rename is longer than the three keys the guides name.
The list is not three keys, it is a query
The standard checklist is wp_user_roles in options, plus wp_capabilities and wp_user_level in user meta. Those are the core ones, and they are the ones that lock you out loudly.
They are not the only prefixed keys. Plugins store their own, and nothing stops them: the prefix is just a string a plugin can read from $wpdb->prefix and concatenate.
So do not work from a checklist. Ask the database what is actually there, on a copy, before you touch production:
SELECT option_name FROM xk3_options WHERE option_name LIKE 'wp\_%';
SELECT DISTINCT meta_key FROM xk3_usermeta WHERE meta_key LIKE 'wp\_%';
Run that after the rename. Anything it returns is a key that WordPress or a plugin will now look for under the new prefix and fail to find.
Over SSH on a sandbox, WP-CLI is already installed, so this is one command rather than a phpMyAdmin session:
sandywp ssh mysite --cmd "wp db query \"SELECT option_name FROM xk3_options WHERE option_name LIKE 'wp\\_%'\""
The backslash before the underscore matters. In a LIKE pattern an unescaped _ matches any single character, so 'wp_%' also matches wpseo_titles and a dozen other options that have nothing to do with your prefix. That false positive sends people renaming things they should not touch.
The failure that does not lock you out
The loud failure is easy. You cannot log in, so you know immediately, and you fix the two keys.
The quiet failure is a plugin with the prefix written into its SQL as a literal string. $wpdb->prefix follows the rename automatically. A hardcoded wp_postmeta in a custom query does not.
That query now points at a table that no longer exists. It returns an error, $wpdb returns false or an empty result, and the calling code usually treats that as "no rows" rather than "broken".
Which means the front page is fine. The failure surfaces only on the code path that runs that query, and that might be a report screen, a weekly cron job, or an importer nobody touches until next month.
You cannot find this by clicking around. You find it by reading the code:
grep -rn --include=*.php -E "['\"\`]wp_(posts|options|users|usermeta|postmeta|terms|comments)" wp-content/
Every hit is a query that will not survive the rename. On a mounted sandbox you can run that with your normal editor's search instead, over sandywp mount.
Be honest about what this misses. A prefix assembled at runtime, or one sitting in a plugin's own settings row, will not appear in a grep for a literal string.
Prefixes hiding inside serialized values
The other place the old prefix survives is inside a serialized array in wp_options, where a plugin stored a table name or a column mapping as configuration.
A targeted UPDATE ... SET option_name = ... does not touch values, so those are untouched by the rename and stay wrong.
They are also the values you must not fix with a careless find and replace. Editing a serialized string by hand breaks its byte counts, and PHP then refuses to unserialize the whole array. We wrote up how to check for exactly that kind of silent corruption and it applies here without changes.
Multisite makes this considerably worse
On multisite the tables are not all prefixed the same way, and this is where a routine that worked on a single site quietly does the wrong thing.
Per-site tables use the base prefix plus the site ID: wp_2_posts, wp_2_options. The wpdb documentation describes $base_prefix as "the original prefix as defined in wp-config.php" and notes it is what you want when you need the prefix "without the blog number appended".
But users and usermeta are global tables. They use the base prefix only, with no site number, and they are shared by the whole network.
So the capability keys for every subsite live together in that one global wp_usermeta table: wp_capabilities for site 1, wp_2_capabilities for site 2, and so on. Rename only wp_capabilities and you have restored access to the main site while leaving every subsite's users with no role.
There is a matching wp_2_user_roles option in each subsite's own options table, too. That is one more key per site, in a different table per site.
If the network has thirty sites, this is sixty keys and it is not a job for a checklist. Generate the statements from the site list, and test the whole thing on a network clone before you go near production.
A routine that actually catches these
Do all of this on a throwaway copy. The Cloner plugin puts your real site into a sandbox in one click, and a real copy is the point here: the bugs live in your specific plugins and your specific data, not in WordPress.
- Clone the site to a sandbox. Record a baseline:
wp db query "SHOW TABLES"and count them. - Grep
wp-content/for the literal prefix, as above. Fix or flag every hit before renaming anything. - Rename the tables, then set the config with
wp config set table_prefix xk3_ --type=variable. Order matters only in that the site is broken between the two steps, which on a sandbox is fine. - Run the two
LIKE 'wp\_%'queries. Rename every key they return, then run them again and require an empty result. - Count tables again. The number must match the baseline, and nothing may still carry the old prefix.
- Now exercise the site. Log in as a non-administrator, not just as yourself. Save a post, run the checkout or the form or whatever the site is actually for, and trigger the cron jobs with
wp cron event run --due-now. - Turn on WP_DEBUG and the debug log for all of step 6. A query against a missing table shows up there as a WordPress database error, and that is the only place the quiet failure announces itself.
Step 7 is the one people skip and the one that pays. Everything else confirms the rename; only the log confirms the code survived it.
If you do this often, for clients or across a fleet, save the configured sandbox as a template so the next run starts from the same known state instead of a fresh clone.
Should you change the prefix at all?
The honest answer, which most of these guides avoid, is that the security benefit is small.
The usual claim is that a non-default prefix stops automated SQL injection attempts. It raises the effort a little, because a canned payload that names wp_users misses. It does not stop an attacker who can run a query, because a database user that can read your tables can generally also read information_schema.tables and simply ask for the names.
So it is a marginal, one-time obstacle. It is not a control you should count on, and it is not worth a lockout or a broken cron job on a live site.
If a security audit requires it, do it, and do it with the verification above. If you are doing it because a listicle suggested it, patch your plugins instead. That is where the actual risk is.
What a sandbox does not test
A sandbox has your data and your plugins, so it catches the code and schema problems. It does not reproduce everything.
It will not reproduce your host's database user privileges. If the production MySQL user lacks ALTER on some tables, the rename fails there and succeeded on your copy.
It will not reproduce scale. A RENAME TABLE on a large InnoDB table is fast, but a plugin's repair routine firing afterwards on two million rows is not, and an empty copy finishes instantly.
And it will not reproduce concurrency. On a live site, requests keep arriving during the window when the tables are renamed and the config is not, and every one of them sees a broken site. Plan the maintenance window on that basis.
If you are doing this for a client site rather than your own, our notes for agencies cover the wider pattern of testing a destructive change on a copy first.
