Get a Free Quote

wp_options autoload bloat: why every page on your site is slow

Here is a WordPress performance problem that no caching plugin fixes and most speed guides never mention. If every page on your site is slow by roughly the same amount, including pages with almost nothing on them, and the admin feels sluggish even on a fast server, the cause is often a single database query that runs before anything else: the one that loads your autoloaded options. On a healthy site it returns a few hundred kilobytes. On a site that has had plugins installed and removed for a few years it can return several megabytes, on every single request, uncached. This guide shows how to measure it, how to find what put it there, and how to clean it without breaking the site.

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:

  1. The cost is paid on every request, whether or not anything reads the option.
  2. 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.

Thinking about a new WordPress website?Get a free consultation and a fixed-scope quote. A senior engineer replies within 24 hours. No obligation.
Get a Free Quote

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 seeWhat it usually isRight action
A huge option named after a plugin you do not haveLeftover from an uninstalled pluginSafe to delete once confirmed the plugin is gone
Thousands of _transient_ rowsExpired cache that was never cleanedClean, then fix WP-Cron
_transient_ rows with no matching timeout rowTransients stored without expiryDelete the orphans; report to the plugin author
A large option holding logs or historyA plugin writing its log to an optionSet autoload to no, then turn the logging down
Serialised arrays of licence or API responsesA plugin caching remote calls in an optionSet autoload to no
A very large option from a page builder or themeGlobal settings or CSS stored as one blobSet autoload to no and test carefully
A large unrecognisable encoded blobPossibly a compromise, not performanceStop 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.

  1. 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.
  2. 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.
  3. 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 --all

If 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 test

A 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 --quiet
Ready to bring your WordPress project to life?Get a free consultation and a fixed-scope quote. A senior engineer replies within 24 hours. No obligation.
Get a Free Quote

Cleaning 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 --expired

Then 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_option

Prefer 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 optimize

Deleting 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.

Hamza Hai

Hamza Hai writes about WordPress development, performance, and growth for businesses.

FAQ

Frequently asked questions

WordPress loads every option marked autoload on every request, in one query, before your theme or plugins run. When that set grows to megabytes, every page pays the cost. It is commonly caused by plugins storing large data as autoloaded options and by removed plugins leaving their options behind.

Under about 400KB is comfortable and under 1MB is usually tolerable. Past a megabyte you can normally feel it, and several megabytes is a serious problem. Treat the figure as a signal to investigate rather than a hard threshold, because what matters is which options are large and whether they need to be autoloaded at all.

Not for logged-in users, the admin, or anything that bypasses the page cache, which includes most WooCommerce cart and checkout activity. Page caching hides the symptom for anonymous visitors while the underlying cost remains on every uncached request.

Deleting is risky; changing autoload to no is usually safe and reversible. The safe order is to back up, set large non-critical options to autoload no, test, and only then delete options that clearly belong to plugins you no longer have installed.

Expired transients are only cleaned opportunistically, and if WP-Cron is not running reliably they accumulate indefinitely. Transients stored without an expiry are autoloaded and never cleaned up at all, which is the worst case.

A persistent object cache such as Redis removes the repeated database query, which helps a great deal. It does not reduce the data itself, and it can hide the problem until the cache is cleared or fails, at which point the site is slow again.

Have a project?

Let's Build Your Next WordPress Website

Get a free consultation and a fixed-scope quote. No obligations.

  • Free Consultation
  • No Hidden Costs
  • 100% Confidential

Request your free quote

Tell us what you are building. A senior engineer replies within 24 hours.

Please enter your name.

Please enter a valid email address.

Please tell us a little more about your project (10+ characters).

No obligation. Your details are only used to prepare your quote.