Turbopress Database Diagnostics

Description

Turbopress Database Diagnostics is a diagnostic tool, not a cleaner. It answers a question the usual optimisation plugins do not: why is this installation slow, and which plugin is causing it.

Most sites do not need another delete button. They need to know that 600 KB of options are read and unserialized on every single request, and which plugin put them there.

Autoload with the plugin responsible

Every WordPress request reads all autoloaded options before a single line of the page is rendered. This plugin measures that payload exactly, lists the 25 largest entries, and attributes each one to the plugin that created it.

Attribution works by matching option name prefixes against the plugins installed on the site. It matches strictly: a prefix only counts when the option name continues with a separator, so acf_field_group is attributed to Advanced Custom Fields while acfxtra_setting is not. Anything that cannot be attributed is reported as not attributed rather than assigned to the nearest plausible plugin. Guessing would be worse than saying nothing.

The unattributed remainder is grouped by its shared prefix, so instead of one anonymous lump you see, for example, that six options starting with ionos account for 186 KB. Grouping is not attribution and is labelled as such, but it turns a dead end into something you can search for.

Options whose plugin is no longer installed are reported as their own category. So are theme modifications of themes that have been deleted. These are the leftovers no uninstall routine removed.

Object cache, OPcache and page cache in one picture

  • Detects whether a persistent object cache is active and which backend serves it: Redis, Valkey, Memcached or APCu. Where none is active, the report states what is actually known: that the PHP extension is loaded means the client side is ready, not that a cache server exists. On shared hosting the extension is usually compiled in for every account while the server runs per account, or not at all, and the plugin does not pretend to know which.
  • Reads Redis and Valkey statistics through the connection the drop-in already holds. The plugin never opens its own connection and never asks for host, port or credentials. Reported are hit rate, memory use, fragmentation ratio, evicted keys and the maxmemory-policy.
  • Reads the origin of the object-cache.php and advanced-cache.php drop-ins from their file header.
  • Warns when two plugins are doing the same caching job. A page cache beside an object cache is the normal arrangement and is not reported as a conflict.
  • Reports OPcache memory, utilisation, hit rate and wasted memory, and tells apart the states that look identical from the outside: not installed, switched off, running but with opcache_get_status in disable_functions, running but with opcache.restrict_api locking this path out, and measured from the command line where OPcache is off anyway. Only the first two are faults of the site; the rest are hosting arrangements and are left out of the score rather than counted against you. Where the counters are unreadable the plugin still asks opcache_is_script_cached whether a WordPress core file is in the cache, so it can say whether OPcache is actually working instead of guessing from a configuration flag.
  • Identifies a page cache from the response headers of a loopback request to your own home URL, and tells a cache that answered from one that was merely present and did not serve. When a cache did answer, a second request with a cache busting parameter measures how long the same page takes when no cache can answer it. That turns « your site would be slow without its page cache » from an assertion into a number. Both requests can be switched off in the settings.

In the Site Health screen

Three findings are also placed in Tools Site Health, where people already look: the autoload payload, what removed plugins left behind, and orphaned metadata. Only those three, and only because WordPress does not check them itself; its own object cache and cron tests are not repeated. Site Health never triggers a scan, it reads the last one.

What changed since last time

Each measurement is recorded, thirty of them, in an option that is not autoloaded. The report then says what moved: « +180 KB since the measurement three days ago ». That answers the question a single snapshot cannot, which is since when, and knowing what you installed that week usually names the cause.

It also checks whether the options table has the index on the autoload column that WordPress 6.6 introduced. An installation that grew out of an older version sometimes lacks it, and without it the first query of every request scans the whole table.

The verdict

From these measurements the plugin names the one factor that costs the most time before the first byte, in plain language and with the numbers it is based on.

It also states the interaction administrators most often get wrong: a persistent object cache removes the database query for your options, but not what follows it. The decoded options are held in memory for the whole request, on every request, cache or no cache.

The plugin measures both halves of that cost on your server rather than asserting them. On a 590 KB payload the decoding itself took 0.9 ms, which is not worth worrying about, while the decoded result occupied 2.0 MB of memory, three and a half times its size on disk. That is the number that matters on a host with a tight memory limit, and it is the reason a large autoload payload hurts.

Scheduled events

WordPress has no scheduler of its own; it keeps the list of due work in a single autoloaded option and looks at it when someone visits. The plugin reads that list and reports three things that go wrong with it: an event scheduled hundreds of times over because a plugin rescheduled without unscheduling first, events overdue by days because nothing is triggering cron at all, and events belonging to plugins that are long gone. Every hook is attributed to its plugin the same way options are.

Database bloat

Orphaned post, term, comment and user meta, post revisions, expired transients, auto-drafts and trashed posts. A table overview from INFORMATION_SCHEMA with size, engine, row count, collation and free space. Tables whose prefix belongs to a plugin that is not installed.

Free space is reported per engine rather than as one number, because DATA_FREE does not mean the same thing everywhere. In MyISAM it is waste that OPTIMIZE TABLE returns to the file system. In InnoDB it is space the engine reuses for new rows, so it is reported separately and never called reclaimable. And with innodb_file_per_table switched off it describes the shared tablespace, so the same figure appears on every table; there the plugin leaves it out and says why, rather than adding one number up thirty times.

Collations are compared against the WordPress tables, not against the majority: on a site with many plugin tables the plugins outnumber core, and a plain majority would report WordPress itself as the anomaly. Tables still on the three byte utf8 or utf8mb3 are called out separately, because those cannot store emoji and make joins against utf8mb4 tables fail outright.

On large tables the exact count is attempted under a deadline enforced by the database server. Only when that deadline passes does the plugin fall back to a bounded sample, and the result then says so. Because orphans are created by events and therefore sit in contiguous blocks rather than spread evenly, a sample can be genuinely inconclusive; in that case the plugin reports a confirmed lower bound and states that the total could not be determined, instead of printing a number that looks measured.

WooCommerce data is treated with care. Orders, their metadata and active sessions are never counted as orphaned, trashed orders are never offered for deletion, and while WooCommerce keeps its orders in its own tables (HPOS), post meta is reported but not offered for cleanup at all: whether a row still belongs to an order cannot be answered reliably then, and that is not a basis for an irreversible delete.

Cleanup

Cleanup exists, but it is a side function, not the selling point. Every category runs on its own, never all at once. Dry run is the default: you see how many rows are affected and confirm before anything is deleted. Work is batched over the REST API with a progress display. There is no cron job and no automatic cleanup.

On the command line

Everything the screen does is available through WP-CLI, because the scanners
were written to know nothing about the admin screen.

wp turbopress-diagnostics scan                    # score and the figures behind it
wp turbopress-diagnostics scan --refresh          # measure again first
wp turbopress-diagnostics report > report.json    # the full document
wp turbopress-diagnostics categories              # what can be cleaned, and how much
wp turbopress-diagnostics cleanup orphan_postmeta            # simulate
wp turbopress-diagnostics cleanup orphan_postmeta --execute  # delete, asks first

That turns a report across many installations into a shell loop, which is the
difference between a tool for one site and a tool for a hosting account.

What this plugin does not do

  • No upsell, no pro version, no locked features. What you install is the complete plugin.
  • No advertising and no external links in the interface.
  • No telemetry, no tracking, no phoning home.
  • No external HTTP requests, with one exception: the optional loopback requests to your own home URL that detect a page cache and measure the uncached response time. They can be switched off in the settings.

Screenshots

Installation

  1. Upload the plugin to /wp-content/plugins/turbopress-database-diagnostics/ or install it through Plugins Add New.
  2. Activate it.
  3. Open Tools Database Diagnostics.

Scans never run during regular page loads. A result is kept for 12 hours and refreshed only when you press the rescan button.

Version 1.0 reports on a single site. On a multisite network, activate it per site; network activation is refused with an explanation rather than producing numbers that look network wide but are not.

FAQ

Is it safe to run this on a production site?

Reading is safe. The scan runs two queries against the options table and a handful of aggregate queries against INFORMATION_SCHEMA, all of them bounded. Nothing runs during normal page loads.

Cleanup deletes rows, and deleting rows is never risk free. Every operation shows you the affected row count first and requires confirmation. Make a backup before you delete anything.

Why does it say « not attributed » instead of naming a plugin?

Because it does not know. Option names carry no author. Attribution is derived from prefixes, and when a prefix matches nothing on the installation, the honest answer is that the origin is unknown. A guess would send you looking in the wrong place.

A plugin of mine is not recognised. Can I register its prefix?

Yes. The prefix table is filterable:

add_filter(
    'turbopress_diagnostics_plugin_prefixes',
    function ( $prefixes, $plugins, $themes ) {
        $prefixes['acme'] = array(
            'type'  => 'plugin',          // plugin, mu_plugin, theme,
                                          // removed_plugin, removed_theme
            'id'    => 'acme-widgets',    // directory slug
            'label' => 'Acme Widgets',    // name shown in the report
        );

        return $prefixes;
    },
    10,
    3
);

The array is keyed by prefix without a trailing separator. $plugins contains the installed plugins and must-use plugins keyed by slug, $themes the installed themes keyed by stylesheet, so you can decide whether to register anything at all. Entries that do not follow this shape are discarded, because a malformed entry would produce a wrong statement about who is responsible for a payload.

If you are a plugin author whose options are showing up as unattributed on other people’s sites, adding this filter to your own plugin fixes it for every user at once.

Can I use this from WP-CLI?

Yes, and that is the point of it for anyone looking after more than one site. wp turbopress-diagnostics scan prints the score and the figures behind it, wp turbopress-diagnostics report the full JSON document, wp turbopress-diagnostics categories what can be cleaned up, and wp turbopress-diagnostics cleanup <category> runs one category. Cleanup simulates unless you add --execute, and asks for confirmation unless you add --yes.

wp help turbopress-diagnostics lists every subcommand and option. The commands are also shown on the plugin screen, under the settings.

Which other filters does the plugin provide?

turbopress_diagnostics_loopback_sslverify decides whether the loopback request verifies the TLS certificate. Return false on a staging environment behind a self signed certificate, where the check would fail for a reason unrelated to caching.

turbopress_diagnostics_table_prefixes registers table name prefixes so leftover tables of a plugin that is gone are named rather than ignored, in the same way as the option prefixes above.

turbopress_diagnostics_count_time_budget sets how many seconds an exact orphan count may take on a large table before it falls back to a sample. The default is 5. The deadline is enforced by the database server, so an overrunning query is stopped rather than left to consume the PHP execution time. Raise it to trade a slower scan for an exact number.

The plugin cannot read my OPcache statistics. Is something broken?

Probably not. Managed hosting commonly puts opcache_get_status into disable_functions, or restricts it with opcache.restrict_api. OPcache keeps working; only the counters are unreadable. The plugin tells that apart from a genuinely disabled OPcache, leaves it out of the score instead of counting it against your site, and still reports the configuration through opcache_get_configuration, which usually survives. It also asks whether a WordPress core file is currently in the cache, which answers whether OPcache is doing its job even when the numbers are hidden.

Does the loopback request send data anywhere?

No. It is a single GET request to your own home URL, made by your own server, to look at the response headers. Nothing is sent to any third party. You can switch it off in the settings, in which case page cache detection is skipped.

Why does it need Redis credentials?

It does not. Redis and Valkey statistics are read through the connection the object cache drop-in has already established. If the drop-in does not expose its client, the plugin reports that no statistics are available rather than trying to connect on its own.

Can I export the result?

Yes, as JSON. This is meant for support tickets: it contains the measurements and option names, no option values.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“Turbopress Database Diagnostics” is open source software. The following people have contributed to this plugin.

Contributors

Changelog

1.0.3

  • Table and column names now go through the %i placeholder of $wpdb->prepare() instead of being quoted by hand and interpolated into the query. This raises the requirement to WordPress 6.2, which is where %i arrived.
  • The admin script handle, the JavaScript object it receives and the CSS classes carry the plugin prefix instead of the short tpdiag.

1.0.2

  • Renamed to Turbopress Database Diagnostics. The rename covers the text domain, the menu, the REST namespace, the WP-CLI command (now wp turbopress-diagnostics) and the four public filters, which are now turbopress_diagnostics_*.
  • The compiled German catalogue is no longer bundled; translations come from translate.wordpress.org.

1.0.1

  • New: transient rows that a persistent object cache has made unreachable are reported and can be removed. While a drop-in is in charge, WordPress keeps every transient in the cache and never looks at the option table, so rows that are in it anyway are leftovers from before the cache was switched on, and the transient API can no longer remove them.
  • New: orphaned timeout rows under update_core, update_plugins or update_themes are marked separately, because WordPress skips the expiry lookup for those three names and so never clears them by reading them.
  • Fixed: two statements about what WordPress can clean up itself were wrong. A timeout row without a value is removed by reading the same transient again after it has expired, and the reason the delete function leaves it behind is the return value of the delete it attempts first, not a read.

1.0.0

  • First release.