How to Reconstruct a Dead Plugin’s Database Footprint Before Uninstalling

When a plugin stops getting updates, throws fatal errors on modern PHP, or just vanishes from the repository, the first impulse is to delete it and move on. That impulse is wrong for anyone running a production WordPress install. A dead plugin is not just a folder in wp-content/plugins. It is a set of rows in wp_options, a possible custom table or two, user meta entries, capabilities, cron events, and sometimes transients that still fire. If you uninstall without mapping that footprint, you leave orphaned data that can slow queries, confuse future migrations, or create conflicts with replacement plugins. This article shows how to reconstruct that footprint using SQL, WP-CLI, and the WordPress database schema itself before you remove anything.

This matters for small-to-mid publishing teams that maintain their own installs. You do not have a staging environment with a dedicated DBA. You have a production database, a backup plugin, and a terminal window. The goal is not to preserve dead code. The goal is to know exactly what the dead code left behind, so the uninstall is clean and reversible.

Start with the plugin’s declared schema, not its folder

Before touching the database, read the plugin’s main file and its uninstall routine. Many plugins register tables, options, and cron hooks in the main PHP file. Even if the plugin is dead, the code is still on disk and still readable. Look for register_activation_hook, register_deactivation_hook, register_uninstall_hook, and any dbDelta calls. These tell you what the plugin intended to create.

grep -R "register_activation_hook\|dbDelta\|add_option\|update_option\|wp_schedule_event" wp-content/plugins/dead-plugin/

This is not a complete map. Plugins often create options lazily, only when a feature is used. But the declared hooks give you the first layer: the plugin’s own assumptions about its footprint. Write those down. You will compare them against what actually exists in the database.

Inventory options with a prefix pattern

Most plugins store settings in wp_options using a consistent prefix. The prefix is usually the plugin slug or an abbreviation. If the plugin is called “Old Gallery Pro,” the options might be ogp_, old_gallery_, or ogpro_. You can find the prefix by grepping the plugin source for get_option and update_option calls.

grep -R "get_option\|update_option" wp-content/plugins/dead-plugin/ | head -50

Once you have candidate prefixes, query the options table. This query returns every option whose name starts with a given prefix, along with its autoload status and a truncated value. Autoload status matters because large autoloaded options are loaded on every request, even after the plugin is gone.

SELECT option_name, LENGTH(option_value) AS value_length, autoload
FROM wp_options
WHERE option_name LIKE 'ogp\_%'
ORDER BY option_name;

The underscore in the LIKE pattern is escaped because _ is a single-character wildcard in SQL. If you forget the backslash, you will match ogpX and ogp1 as well. That is a real failure mode when you are cleaning up after a plugin with a short prefix.

Do not stop at one prefix. Some plugins use multiple prefixes for different subsystems. A gallery plugin might use ogp_ for settings, ogp_album_ for album metadata, and ogp_cache_ for cached thumbnails. Grep the source for all get_option calls and collect every distinct prefix.

Find orphaned custom tables

Plugins that store large datasets often create custom tables. The table names usually follow the WordPress prefix, so a plugin with the slug old-gallery-pro might create wp_ogp_albums and wp_ogp_photos. To find them, list all tables that are not part of the core WordPress schema.

SELECT table_name
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name NOT IN (
  'wp_commentmeta', 'wp_comments', 'wp_links', 'wp_options',
  'wp_postmeta', 'wp_posts', 'wp_term_relationships',
  'wp_term_taxonomy', 'wp_termmeta', 'wp_terms',
  'wp_usermeta', 'wp_users'
);

This returns every non-core table, including tables from other plugins and any custom tables you created yourself. You need to match the table names against the dead plugin’s source. Grep for CREATE TABLE and $wpdb->prefix in the plugin folder.

grep -R "CREATE TABLE\|\$wpdb->prefix" wp-content/plugins/dead-plugin/

If the plugin used dbDelta, the table creation statements are usually in an includes or admin subfolder. The table names will be concatenated from $wpdb->prefix and a literal string. That literal string is what you look for in the information_schema output.

Before dropping any table, export it. A dead plugin’s table might contain data you need for a migration, or it might be the only record of a content relationship that a replacement plugin needs to rebuild. Use mysqldump or WP-CLI to export the table to a file, then store that file outside the web root.

wp db export --tables=wp_ogp_albums,wp_ogp_photos /tmp/dead-plugin-tables.sql

Trace user meta and capabilities

Plugins that add roles or capabilities write to wp_usermeta and wp_options. A membership plugin might add a wp_capabilities entry for a custom role, or a wp_user_level value. A plugin that stores per-user preferences writes to wp_usermeta with a key like ogp_user_settings.

To find user meta left by the dead plugin, query for keys that match the plugin’s prefix or slug.

SELECT user_id, meta_key, meta_value
FROM wp_usermeta
WHERE meta_key LIKE '%ogp%'
OR meta_key LIKE '%old_gallery%'
ORDER BY meta_key, user_id;

Capabilities are trickier. They are stored as serialized arrays in wp_options under the key wp_user_roles, and as per-user serialized arrays in wp_usermeta under wp_capabilities. If the dead plugin registered a custom role, that role is still in the wp_user_roles option. You can inspect it with WP-CLI.

wp role list --fields=role,name

If you see a role that only the dead plugin used, note it. Removing the role is not as simple as deleting the option. You need to remove the role from every user who has it, then remove the role definition. WP-CLI can do this, but only after you have confirmed no other plugin or theme depends on that role.

wp role exists ogp_editor
wp user list --role=ogp_editor --fields=ID,user_login

If the role exists and has users, reassign those users to a standard role before removing the custom role. Otherwise you leave users with a capability set that no longer resolves to a defined role, which can cause unexpected access denials or, worse, unexpected access grants if a future plugin reuses the same role slug.

Check cron events and scheduled tasks

Dead plugins often leave scheduled events in wp_options under the cron option. These events fire on every page load if their scheduled time has passed, and they call functions that no longer exist. That produces PHP warnings in your error log and, in some cases, fatal errors that take down the site.

List all scheduled events and look for hooks that match the dead plugin’s slug or function names.

wp cron event list --fields=hook,next_run_relative,recurrence

If you see a hook like ogp_daily_cleanup or old_gallery_sync, that is a leftover. You can remove it with WP-CLI.

wp cron event delete ogp_daily_cleanup

But before deleting, check whether the hook is registered anywhere else. A theme or a must-use plugin might have taken over the hook. Grep the entire wp-content directory for the hook name.

grep -R "ogp_daily_cleanup" wp-content/

If the only match is in the dead plugin’s folder, the event is safe to remove. If the hook appears in a theme or another plugin, you need to understand that dependency before deleting the event.

Inspect transients and object cache leftovers

Transients are stored in wp_options with a _transient_ prefix. They expire, but a dead plugin’s transients might have long expiration times or might be set to autoload. A plugin that cached external API responses might have left hundreds of transients that are still being loaded on every request.

SELECT option_name, LENGTH(option_value) AS value_length, autoload
FROM wp_options
WHERE option_name LIKE '\_transient\_ogp%'
OR option_name LIKE '\_transient\_timeout\_ogp%'
ORDER BY option_name;

The _transient_timeout_ entries are companion rows that store the expiration timestamp. If you delete the transient but not the timeout, WordPress will try to read a missing transient and then delete the orphaned timeout on the next request. That is harmless but messy. Delete both.

DELETE FROM wp_options
WHERE option_name LIKE '\_transient\_ogp%'
OR option_name LIKE '\_transient\_timeout\_ogp%';

If you use an object cache like Redis or Memcached, transients might be stored there instead of the database. Flush the object cache after deleting the database rows, or the old values will be served until the cache expires.

wp cache flush

Map post meta and taxonomy terms

Plugins that extend content types often write to wp_postmeta and wp_term_taxonomy. A gallery plugin might store image metadata in wp_postmeta with keys like _ogp_image_id or _ogp_album_order. A plugin that adds custom taxonomies might have registered a taxonomy that is still present in wp_term_taxonomy.

Find post meta keys that match the plugin’s prefix.

SELECT meta_key, COUNT(*) AS row_count
FROM wp_postmeta
WHERE meta_key LIKE '\_ogp%'
OR meta_key LIKE 'ogp%'
GROUP BY meta_key
ORDER BY row_count DESC;

Do not delete these rows yet. Some post meta is used by the block editor or by other plugins that read the same keys. A replacement gallery plugin might import the old plugin’s post meta to rebuild galleries. Export the rows first, then decide whether to delete them.

For taxonomies, check wp_term_taxonomy for taxonomy names that match the dead plugin.

SELECT taxonomy, COUNT(*) AS term_count
FROM wp_term_taxonomy
WHERE taxonomy LIKE '%ogp%'
OR taxonomy LIKE '%old_gallery%'
GROUP BY taxonomy;

If the dead plugin registered a custom taxonomy, the terms are still in wp_terms and wp_term_taxonomy. The taxonomy itself is registered in code, so once the plugin is deleted, the taxonomy no longer exists. But the term rows remain. They are orphaned data. You can delete them, but first check whether any posts are still assigned to those terms.

SELECT p.ID, p.post_title
FROM wp_posts p
INNER JOIN wp_term_relationships tr ON p.ID = tr.object_id
INNER JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
WHERE tt.taxonomy = 'ogp_album'
LIMIT 50;

If posts are assigned to the dead taxonomy, you need to decide what to do with those assignments. Deleting the terms will remove the assignments, but the posts themselves remain. That is usually the correct outcome, but only after you have confirmed the posts do not rely on the taxonomy for display or routing.

Reconstruct the full footprint in a single report

You can combine these queries into a single WP-CLI command or a SQL script that outputs a complete footprint report. The report should include options, tables, user meta, cron events, transients, post meta, and taxonomy terms. This is the document you review before uninstalling.

wp db query "SELECT 'options' AS type, option_name AS name, LENGTH(option_value) AS size, autoload AS extra FROM wp_options WHERE option_name LIKE 'ogp\\_%' UNION ALL SELECT 'tables', table_name, 0, '' FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'wp\\_ogp%' UNION ALL SELECT 'usermeta', meta_key, 0, '' FROM wp_usermeta WHERE meta_key LIKE '%ogp%' UNION ALL SELECT 'postmeta', meta_key, COUNT(*), '' FROM wp_postmeta WHERE meta_key LIKE '\\_ogp%' GROUP BY meta_key;"

The escaping in this query is ugly because WP-CLI passes the SQL through a shell. If you are running this from a SQL client, you can simplify the escaping. The point is to produce one output that shows every database object the dead plugin touched.

Save that report to a file. It is your rollback plan. If the uninstall breaks something, the report tells you exactly what to restore.

What to do before you click delete

Once you have the footprint report, take a full database backup. Do not rely on the report alone. A backup is the only way to restore the exact state if something goes wrong.

wp db export /backups/pre-uninstall-dead-plugin-$(date +%Y%m%d).sql

Then deactivate the plugin, but do not delete it. Deactivation triggers the plugin’s deactivation hook, which might clean up some data or leave it in a different state. Check the footprint again after deactivation. If the deactivation hook removed some options or cron events, your uninstall plan changes.

Only after deactivation and a second footprint check should you delete the plugin. And even then, prefer deleting via WP-CLI or the admin interface, not by removing the folder over FTP. The admin delete process runs the plugin’s uninstall hook if it has one. That hook might clean up data you would otherwise have to remove manually.

wp plugin deactivate dead-plugin
wp plugin delete dead-plugin

After deletion, run the footprint queries again. Anything that remains is orphaned data. You can now remove it manually, using the report as your checklist.

Common failure modes when skipping this process

The most common failure is autoloaded options. A dead plugin that stored a large serialized array in wp_options with autoload = 'yes' continues to load that array on every request. If the array is a few megabytes, your site’s memory usage stays elevated forever. You can find the worst offenders with this query.

SELECT option_name, LENGTH(option_value) AS value_length
FROM wp_options
WHERE autoload = 'yes'
ORDER BY value_length DESC
LIMIT 20;

If any of the top entries match the dead plugin’s prefix, that is your smoking gun. Delete them after the uninstall.

Another failure mode is a leftover cron event that calls a missing function. WordPress fires the event, the function does not exist, and PHP logs a fatal error. If the event is scheduled to run frequently, your error log fills up and your site’s performance degrades. The fix is to delete the event, but only after confirming the hook is not registered elsewhere.

A third failure mode is a custom table that a replacement plugin tries to reuse. If the replacement plugin has the same table name but a different schema, the old table causes a conflict. The replacement plugin’s activation routine might fail, or it might write data into a table with the wrong columns. Dropping the old table before installing the replacement prevents this.

When to keep the data instead of deleting it

Not every orphaned row should be deleted. If you plan to migrate to a replacement plugin, the old plugin’s data might be the only source of truth for content relationships, user preferences, or historical records. In that case, export the data and keep the export file. You can delete the database rows after the migration is complete and verified.

If the dead plugin stored content in custom tables, those tables might contain data that belongs in wp_posts or wp_postmeta. A migration script can read the old tables and write the data into the new plugin’s format. That script is easier to write if the old tables still exist. So do not drop them until the migration is done.

If the dead plugin registered a custom post type, the posts of that type are still in wp_posts with a post_type value that no longer resolves. Those posts are invisible in the admin unless you register the post type again. You can either delete them or convert them to a standard post type. Converting is often better for SEO, because the posts might have inbound links.

UPDATE wp_posts
SET post_type = 'post'
WHERE post_type = 'ogp_gallery'
AND post_status = 'publish';

This is a destructive operation. Back up first. And check whether the posts have meta boxes or taxonomies that only make sense for the old post type. Converting the post type does not convert the meta.

Document the footprint for the next person

After the uninstall is complete, write a short note in your team’s internal documentation. Include the plugin name, the date, the footprint report, and what you deleted. If a future team member wonders why a certain option is missing or why a table no longer exists, the note answers the question.

This is not busywork. Production WordPress installs accumulate decisions. A dead plugin’s footprint is a decision someone made years ago. If you do not record the cleanup, the next person has to reconstruct it from scratch. That is wasted time and a real risk of deleting something important.

If your team maintains multiple installs, consider a recurring column or internal checklist for plugin retirements. The same process applies to themes, but themes have a smaller database footprint. The discipline of mapping before deleting is the same.

FAQ

How do I know if a dead plugin left autoloaded options?

Run a query against wp_options that filters for autoload = 'yes' and sorts by value length. If any option names match the dead plugin’s prefix or slug, those are autoloaded leftovers. You can also use WP-CLI to list autoloaded options and their sizes.

wp option list --autoload=yes --fields=option_name,option_value --format=table | head -50

The option_value field is truncated in the table view, but the option names are enough to identify the plugin.

What is the safest order for uninstalling a dead plugin?

Deactivate first, then check the footprint again, then delete via the admin or WP-CLI, then run the footprint queries a third time. The deactivation hook might clean up some data. The uninstall hook might clean up more. Only after both hooks have run should you manually remove what remains. Always take a full database backup before deactivation.

Can I just delete the plugin folder and ignore the database?

You can, but you will leave orphaned rows that continue to affect performance and can cause conflicts later. Autoloaded options still load on every request. Cron events still fire and call missing functions. Custom tables still take up space. If you never install a replacement plugin, the damage is mostly invisible. If you do install a replacement, the orphaned data can cause real conflicts.

How do I find the option prefix for a plugin that is already deleted?

If the plugin folder is gone, you cannot grep the source. Instead, look for option names that contain the plugin’s slug or a likely abbreviation. You can also check your backup files for the plugin’s source code. If you have a full backup from before the deletion, extract the plugin folder from the backup and grep it. If you have no backup, you are guessing. That is why the footprint report should be created before deletion.

For more on diagnosing a WordPress site that returns nothing, see What to Fix First When a New WordPress Site Says Nothing Found. The same diagnostic discipline applies here: check the database before assuming the problem is in the code.

Person reviewing database tables on a laptop screen

Close-up of SQL query results in a terminal window

Team members discussing a cleanup plan around a desk