The symptom, and why it is distinctive
Autoload bloat has a recognisable signature, which is useful because it rules out most other causes quickly.
- Every page is slow by a similar amount. A near-empty page and a complex one are both slow. A heavy page problem would not behave like that.
- The admin is slow too, often more so, because the admin is not page cached.
- Caching helps anonymous visitors and nobody else. Logged-in users, the admin, cart and checkout stay slow.
- Time to first byte is the problem, not rendering. The browser waits, then the page draws quickly.
- A faster server barely helps. You are moving and unserialising megabytes on every request, so more CPU shaves a little off a cost that should not exist.
If instead one page is slow and others are fine, this is not your problem. Look at that page's queries. Our speed guide covers the ordinary causes.
What autoload actually does
The wp_options table has an autoload column. Every row marked yes is fetched in a single query on every request and held in memory for the duration, so that get_option() does not need a database round trip each time it is called.
That design is sound. Site title, active plugin list, permalink structure: a handful of small values needed on every page. The problem is that nothing stops a plugin storing something large that way, and nothing removes those rows when the plugin is deleted.
Two consequences follow, and the second surprises people:
- The cost is paid on every request, whether or not anything reads the option.
- Deleting a plugin usually leaves its options behind. WordPress does not require an uninstall routine, and plugins that register one often only clean their own settings, not their caches. Years of installed-and-removed plugins accumulate.
So a site can carry the autoload cost of a dozen plugins it has not had installed since 2023.
Measuring it
Do not guess. These are read-only queries. Replace the wp_ prefix with the one in your wp-config.php, which is frequently something else.
The total, which is the number that matters
wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024, 2) AS autoload_mb,
COUNT(*) AS rows_autoloaded
FROM wp_options WHERE autoload='yes';"Rough reading of the result: under 0.4MB is healthy, up to 1MB is usually fine, 1 to 3MB is worth fixing, and above 3MB is very likely what you can feel. Row count matters less than total size, though several thousand autoloaded rows is itself a sign something is wrong.
The biggest offenders
wp db query "SELECT option_name, ROUND(LENGTH(option_value)/1024, 1) AS kb
FROM wp_options WHERE autoload='yes'
ORDER BY LENGTH(option_value) DESC LIMIT 30;"This is the query that tells you what to do. In almost every case a handful of rows account for most of the total, and they are usually recognisable as belonging to one or two plugins.
The whole table, for context
wp db query "SELECT COUNT(*) AS total_rows,
ROUND(SUM(LENGTH(option_value))/1024/1024, 2) AS total_mb
FROM wp_options;"
# how many of these are transients
wp db query "SELECT COUNT(*) FROM wp_options WHERE option_name LIKE '%transient%';"A wp_options table with tens of thousands of rows, most of them transients, points at the problem in the next section but one.
Confirm the query is actually the cost
Before changing anything, prove the connection. Install a query monitoring plugin on a staging copy and look at the autoloaded options figure and total query time on a simple page. You are looking for the option load to be a visible share of time to first byte. If it is not, keep looking elsewhere; being rigorous here saves you from a day spent on the wrong thing.
The usual culprits
After doing this a number of times, the large rows are nearly always one of these.
| What you see | What it usually is | Right action |
|---|---|---|
| A huge option named after a plugin you do not have | Leftover from an uninstalled plugin | Safe to delete once confirmed the plugin is gone |
Thousands of _transient_ rows | Expired cache that was never cleaned | Clean, then fix WP-Cron |
_transient_ rows with no matching timeout row | Transients stored without expiry | Delete the orphans; report to the plugin author |
| A large option holding logs or history | A plugin writing its log to an option | Set autoload to no, then turn the logging down |
| Serialised arrays of licence or API responses | A plugin caching remote calls in an option | Set autoload to no |
| A very large option from a page builder or theme | Global settings or CSS stored as one blob | Set autoload to no and test carefully |
| A large unrecognisable encoded blob | Possibly a compromise, not performance | Stop and read the security section below |
That last row matters. Some malware stores its payload in an autoloaded option so it executes on every request without any file looking unusual. If you find a large autoloaded option whose name you cannot trace to anything and whose value is a long encoded string, treat it as a security question first. Our guides to fixing a hacked WordPress site and the Japanese keyword hack cover what to do.
Orphaned transients, the slow leak
Transients are WordPress's way of caching something with an expiry. Without a persistent object cache they are stored in wp_options as a pair of rows: the value, and a _transient_timeout_ row holding the expiry time.
Three things go wrong, and understanding the difference decides the fix.
- Expired transients are not deleted on schedule. Cleanup is opportunistic. An expired transient occupies a row until something happens to clear it, so a site with unreliable WP-Cron accumulates them indefinitely.
- Transients saved with no expiry are autoloaded. A transient with an expiry is not autoloaded; one without is. A plugin caching large values with no expiry is adding directly to the cost on every request.
- Timeout rows outlive their values and vice versa. Interrupted writes leave one half of the pair, and nothing ever collects it.
# how much space transients occupy
wp db query "SELECT COUNT(*) AS rows_count,
ROUND(SUM(LENGTH(option_value))/1024/1024,2) AS mb
FROM wp_options WHERE option_name LIKE '\_transient\_%'
OR option_name LIKE '\_site\_transient\_%';"
# transients that are autoloaded, which means stored without an expiry
wp db query "SELECT option_name, ROUND(LENGTH(option_value)/1024,1) AS kb
FROM wp_options
WHERE autoload='yes' AND option_name LIKE '%transient%'
ORDER BY LENGTH(option_value) DESC LIMIT 20;"Deleting expired transients is safe by definition: they are cache, and anything still needed is regenerated. WP-CLI will do it:
wp transient delete --expired
# if the table is in a bad state, this clears all of them.
# safe but will cause a brief load spike as caches rebuild.
wp transient delete --allIf transients keep piling up, the real fault is WP-Cron. Check it, because cleaning the table repeatedly while cron stays broken is treating the symptom.
wp cron event list --fields=hook,next_run_relative,recurrence
wp cron testA long list of overdue events means cron is not firing. The usual fix is to disable the request-triggered pseudo-cron and run it from the server instead, which is more reliable on a low-traffic site because WordPress's default only runs when somebody visits.
// in wp-config.php
define( 'DISABLE_WP_CRON', true );# then a real cron entry, every five minutes
*/5 * * * * cd /path/to/site && /usr/bin/wp cron event run --due-now --quietCleaning it safely
Back up the database first. Options hold live configuration, and an over-enthusiastic delete can take a licence key, a settings blob or a WooCommerce setting with it. Work on staging if you have it.
The order below is deliberately cautious: it does the reversible things first and the irreversible thing last.
Step 1: stop autoloading what does not need it
This is the highest-value and lowest-risk action. The data stays; it is simply fetched on demand instead of on every request.
# check the current state of one option
wp db query "SELECT option_name, autoload, ROUND(LENGTH(option_value)/1024,1) AS kb
FROM wp_options WHERE option_name='some_big_option';"
# stop autoloading it
wp db query "UPDATE wp_options SET autoload='no' WHERE option_name='some_big_option';"Do them a few at a time and load the site and the admin after each batch. If something breaks, set it back to yes and you have lost nothing. A plugin that genuinely needs an option on every request will simply read it on demand, slightly slower for that plugin and much faster for everything else.
Leave anything you do not recognise alone at this stage, and never touch core options such as siteurl, home, active_plugins or template.
Step 2: clear expired transients
wp transient delete --expiredThen re-measure the total. On a neglected site this alone often removes most of the bloat.
Step 3: delete options from plugins you no longer have
This is the irreversible step, so confirm before deleting. Check the installed plugin list, search the option name, and satisfy yourself the plugin is genuinely gone.
wp plugin list --fields=name,status
# look at the value before deleting anything
wp option get some_orphaned_option
# then, only when you are sure
wp option delete some_orphaned_optionPrefer deleting one option at a time over a pattern match. A DELETE ... LIKE on an options table is how people remove a licence key at four in the afternoon.
Step 4: re-measure, and optimise the table
wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024,2) AS autoload_mb
FROM wp_options WHERE autoload='yes';"
# reclaim the space the deletes freed
wp db optimizeDeleting rows does not necessarily shrink the file on disk, so optimising afterwards is what actually returns the space.
Step 5: consider a persistent object cache
With Redis or Memcached and a drop-in, the autoloaded set is held in memory and the query stops repeating. This is a real improvement and it is worth doing on any busy site.
One caution: it hides this problem rather than solving it. When the cache is cleared, restarted or fails open, every request goes back to the database and the site is slow again, usually at the worst moment. Reduce the data first, then cache it.
Keeping it down
- Measure quarterly. One query, thirty seconds, and it catches the problem while it is small.
- When you remove a plugin, check what it left. Search the options table for its prefix immediately, while you still remember the name.
- Make sure WP-Cron is reliable. Most transient accumulation is a cron problem wearing a database costume.
- Be wary of plugins that log to the database. Debug and activity logs stored as options grow without limit unless something trims them.
- Keep an eye on the count as well as the size. A sudden jump in autoloaded rows after installing something is worth a look before it becomes routine.
A single monitoring query is enough, and it is worth putting somewhere you will actually see it:
wp db query "SELECT COUNT(*) AS rows_autoloaded,
ROUND(SUM(LENGTH(option_value))/1024,0) AS autoload_kb
FROM wp_options WHERE autoload='yes';"This is the kind of thing that goes unnoticed for years because nothing breaks: the site is just slower than it should be, and everyone assumes that is normal. If your site is slow on every page and caching has not helped, run the measurement query at the top of this guide before buying a bigger server.
If the numbers look bad and you would rather not do database surgery on a live site, send us the domain and we will tell you what is in there and what it is costing. Our speed optimization work starts with measurements like these rather than a plugin, and maintenance and support is what stops it coming back.