Back to blog

The wp_options Autoload Trap: Why WordPress Sites Get Slower Over Time

Every WordPress page load queries all autoloaded options at once. Here's how that table quietly bloats, and how to find and fix the offenders.

The wp_options Autoload Trap: Why WordPress Sites Get Slower Over Time

A site that was quick two years ago and feels sluggish now, with no obvious culprit and no recent change, is a familiar complaint. The plugins are the same. The theme is the same. Traffic hasn't changed. But every page takes noticeably longer than it used to, on the front end and in wp-admin alike.

One of the most common causes is invisible unless you go looking for it: the wp_options table has been quietly accumulating autoloaded data, and WordPress dutifully loads all of it on every single request. Plugins add options and rarely clean up after themselves. Expired transients pile up. Nothing breaks, so nothing gets noticed.

WordPress 6.6 added real guardrails around this, including a Site Health warning that surfaces the problem for the first time. Here's how the mechanism works, how to find what's bloating your table, and what to do about it.

TL;DR

  • WordPress loads all autoloaded options in a single query on every request via wp_load_alloptions(). Without a persistent object cache, that query and the unserialization of its entire payload run on every page load.
  • Since WordPress 6.6, core refuses to autoload individual options larger than 150,000 bytes by default, adjustable via the wp_max_autoloaded_option_size filter.
  • WordPress 6.6 also added a Site Health check that flags installs whose total autoloaded data exceeds 800,000 bytes, adjustable via site_status_autoloaded_options_size_limit.
  • The autoload column now stores more than yes/no. Values include on, off, auto, auto-on, and auto-off, distinguishing explicit developer choices from core's heuristics.
  • Expired transients are not reliably cleaned up on a schedule. WordPress's own documentation says core "infrequently cleans out expired transients" and advises deleting them yourself when done.
  • A persistent object cache changes the picture substantially, since wp_load_alloptions() caches its result. It reduces the cost but doesn't excuse the bloat.

How Autoloading Actually Works

Every option in wp_options has an autoload column. Options marked for autoload are fetched together, in one query, at the start of every WordPress request, before your theme or most plugin logic runs.

The function responsible is wp_load_alloptions(). Its behaviour, per WordPress's developer documentation:

  1. It checks the object cache first, via wp_cache_get( 'alloptions', 'options' ).
  2. On a cache miss, it runs a single SELECT pulling option_name and option_value for every row whose autoload value is in the set of autoload-eligible values.
  3. It caches the result with wp_cache_add( 'alloptions', $alloptions, 'options' ).

That design is deliberate and sensible. One query for hundreds of small settings beats hundreds of individual queries. The trap is what happens when "hundreds of small settings" stops being an accurate description.

Without a persistent object cache, the cache is per-request. That query runs, and its entire result is transferred from MySQL and unserialized in PHP, on every single page load. If your autoloaded data is two megabytes, you are moving and unpacking two megabytes before WordPress renders anything, on every request, including admin pages and AJAX calls.

With a persistent object cache (Redis or Memcached), the result is cached across requests, so the database query happens once per cache lifetime rather than once per request. This is a substantial mitigation, and it's part of why object caching helps some sites so dramatically.

It's a mitigation, not a fix. The data still has to be pulled from the cache and unserialized into PHP memory each request, and a bloated alloptions payload is a bloated payload regardless of where it's stored.

What WordPress 6.6 Changed

For years this was a known problem with no visibility in core. WordPress 6.6 addressed both halves.

The 150KB Individual Option Limit

Core now refuses to autoload any single option larger than 150,000 bytes unless a developer explicitly forces it. This directly targets the worst offender pattern: a plugin storing a large serialized data structure, a cached API response, or an entire settings export as one autoloaded option.

The threshold is filterable via wp_max_autoloaded_option_size, which defaults to 150000.

The 800KB Site Health Warning

WordPress 6.6 also added a Site Health check, at Tools → Site Health, that reports the number and total size of autoloaded options and raises a warning when total autoloaded data exceeds 800,000 bytes. The message recommends reviewing and removing what isn't needed.

That threshold is filterable via site_status_autoloaded_options_size_limit, introduced in 6.6.0 with a default of 800000.

WordPress's Advanced Administration Handbook backs this up as general guidance, recommending sites keep total autoloaded data under 800KB.

Important: The Site Health check tells you the total, not which options are responsible. Finding the offenders takes the WP-CLI commands below.

The New Autoload Values

The autoload column used to hold yes or no. It now holds more, and the distinctions matter:

Value Meaning
on Explicitly forced to autoload (true passed to add_option/update_option)
off Explicitly forced not to autoload (false passed)
auto No explicit choice ever made; still autoloads for backward compatibility
auto-on No explicit choice, but core's heuristic decided it should autoload
auto-off No explicit choice, but core's heuristic decided it should not, for example because it exceeds 150KB

On the API side, add_option() and update_option() now accept true, false, or null for the autoload parameter. null is the default and means, per the documentation, "to stick with the initial value or, if no initial value is set, to leave the decision up to default heuristics in WordPress." The legacy 'yes' and 'no' strings still work but were deprecated in 6.7.0.

There's also wp_set_option_autoload_values() for changing autoload state in bulk without touching option values, which is the right tool for a plugin flipping its own options on activation and deactivation.

Finding What's Actually Bloating Your Table

Site Health tells you that you have a problem. WP-CLI tells you what it is.

Get the total first:

terminal
wp option list --autoload=on --format=total_bytes

This returns a single number: total bytes of autoloaded options. Compare it against the 800,000-byte threshold.

Then find the largest offenders:

terminal
wp option list --autoload=on --fields=option_name,size_bytes | sort -k2 -n | tail -20

A note worth flagging, because it trips people up: WP-CLI's --orderby for wp option list only accepts option_id, option_name, and option_value. There is no --orderby=size. size_bytes is a valid output field but not a sort key, which is why the command above pipes to the shell's sort instead. WordPress's own CLI documentation uses exactly this pattern.

Find the biggest transients, using the documented example:

terminal
wp option list --search="*_transient_*" --fields=option_name,size_bytes | sort -n -k 2 | tail

The Transient Problem

Transients are WordPress's built-in mechanism for cached data with an expiry. Plugins use them constantly for API responses, remote data, rate limits, and computed values.

Here's the part that surprises people. When no persistent object cache is present, transients are stored in wp_options. With Redis or Memcached active, they go to the object cache and never touch the database at all. Most WordPress sites don't have a persistent object cache, so most WordPress sites are storing every transient every plugin creates as rows in wp_options.

And they don't reliably get cleaned up. WordPress's own transients documentation is direct about this:

"transient expiration times are a maximum time... WordPress infrequently cleans out expired transients. To prevent expired transients from building up in the database, it's a good practice to always remove your transient once you are done with it and no longer need it."

Expiration means "this transient stops being valid after this time." It does not mean "this row gets deleted at this time." An expired transient sits in wp_options until something removes it.

Core does provide delete_expired_transients(), added in WordPress 4.9.0, which deletes all expired transients. But it isn't running on a reliable recurring schedule, and it has a significant caveat in its own documentation: "This function won't do anything if an external object cache is in use" unless you pass $force_db = true.

The compounding effect: a plugin that sets a transient per product, per API endpoint, or per user session, on a site without object caching, over two years, produces thousands of rows. If any of them are autoloaded, they're being loaded on every request forever.

Cleaning Up Safely

Before touching anything, take a backup. You are editing the table that holds your entire site's configuration.

Step 1: Measure. Run the total_bytes command above and note the number, so you can verify the fix later.

Step 2: Identify. List the top 20 autoloaded options by size. In most cases a handful of entries account for the bulk of the problem.

Step 3: Attribute. Option names usually reveal their owner by prefix. Match each large option to an active plugin. This is important, because the correct action differs depending on the answer.

Step 4: Act, by category.

  • Options from plugins you no longer have installed. These are orphans, left behind by a plugin that didn't clean up on uninstall. Safe to delete.
  • Expired transients. Safe to delete. They'll regenerate if still needed.
  • Large options from active plugins. Don't delete these. Deleting a plugin's live settings breaks the plugin. Instead, consider whether it needs to autoload:
terminal
wp option set-autoload <option_name> off

This changes autoload state without touching the value. Use it when a large option is only read on specific screens rather than on every request.

  • Options you can't identify. Leave them. An unexplained option is not worth guessing about.

Step 5: Verify. Re-run the total, and check Site Health.

Step 6: Consider a persistent object cache. It moves transients out of wp_options entirely and caches the alloptions result across requests. On MagicWP's managed hosting, this layer is handled at the platform level.

Important: Do this on staging first, especially the deletions. MagicWP's one-click staging lets you clone the site, clean it up, confirm nothing broke, and only then repeat on production, and on-demand backups give you a restore point either way.

What Not to Do

Don't run a blanket "optimise database" plugin and assume it handled this. Most focus on post revisions, spam comments, and table overhead. Some clean transients. Few address autoloaded options specifically, and none can tell you whether a large autoloaded option belongs to a plugin you still need.

Don't delete options because the name looks unfamiliar. Plugin option names are frequently non-obvious. If you can't attribute it, leave it.

Don't set every large option to autoload=off indiscriminately. Some genuinely are needed on every request. Turning off autoload for one of those replaces a single bulk query with an individual query per request, which can be worse.

Don't treat an object cache as a reason to skip the cleanup. It reduces the database cost meaningfully, but the payload still gets unserialized into memory on every request.

Frequently Asked Questions

What are autoloaded options in WordPress? Options in the wp_options table marked to load automatically on every page request. WordPress fetches all of them in a single query via wp_load_alloptions() before your theme or most plugins run, so the total size of that data is paid on every request.

How much autoloaded data is too much? WordPress 6.6 added a Site Health check that warns above 800,000 bytes total, and core stopped autoloading individual options over 150,000 bytes by default. Both thresholds are filterable, but they're a reasonable target.

How do I find which options are bloating my wp_options table? Use WP-CLI: wp option list --autoload=on --fields=option_name,size_bytes | sort -k2 -n | tail -20. Note that wp option list doesn't support sorting by size directly, which is why this pipes to the shell's sort.

Is it safe to delete autoloaded options? It depends what they are. Orphaned options from uninstalled plugins and expired transients are safe. Options belonging to active plugins are not, since deleting them removes live settings. For those, change the autoload flag rather than deleting the row.

Do expired transients get deleted automatically? Not reliably. WordPress's documentation states core "infrequently cleans out expired transients" and recommends deleting them yourself. Expiration marks a transient invalid; it doesn't remove the row.

Does Redis or Memcached fix the autoload problem? It helps substantially. wp_load_alloptions() caches its result, so with a persistent object cache the database query runs once per cache lifetime instead of once per request, and transients move out of wp_options entirely. The payload still gets loaded into PHP memory each request, so it reduces the cost rather than eliminating it.

Why did my site get slower over time without any changes? Accumulation is the usual answer. Plugins add options and transients, uninstalled plugins leave options behind, and expired transients build up. Nothing breaks, so nothing is noticed, but every page load pays a little more each month.

How do I change an option's autoload setting? Over WP-CLI, wp option set-autoload <option_name> off. In code, pass false as the autoload argument to update_option(), or use wp_set_option_autoload_values() for bulk changes.

Conclusion

The wp_options autoload problem is a slow leak rather than a break, which is precisely why it goes unfixed for years. Nothing errors. Nothing looks wrong. The site is just a bit heavier every month than it was the month before.

WordPress 6.6 made it visible for the first time, with the 150KB individual limit and the 800KB Site Health warning. The fix is a short, specific process: measure the total, list the biggest offenders, attribute each to a plugin, delete only what's genuinely orphaned, and flip autoload off for the large options that don't need to be there on every request.

Do it on staging, take a backup first, and check the total afterward so you know it worked. A persistent object cache on top of that changes the economics substantially, but it's a reason to clean up with less urgency, not a reason to skip it.

A
Alex
MagicWP
Writing about WordPress, performance, and the infrastructure that makes sites fast.

Get the best of MagicWP in your inbox.

Monthly engineering notes, product updates, and WordPress performance tips. No spam, unsubscribe anytime.

Join 12,000+ builders. We send one email a month.