Autoloaded data in wp_options: how a bloated table slows WordPress

Your hosting report warns that autoloaded options in wp_options are huge, or the WordPress Site Health screen shows a critical warning about autoload size. This article explains what autoload actually is, why a bloated autoload set slows every single request, which measurements matter (and which do not), and how to shrink it without breaking the site.

"Autoloaded options could affect performance" is one of the more cryptic warnings a WordPress hosting dashboard will ever show you. It sounds technical, and it is, but the underlying idea is simple: WordPress preloads a certain set of settings on every page request, that set has grown too big, and every request is now paying the cost. This article explains what autoload actually is, why it grew, what the WP 6.6 release changed about it, and how to shrink it safely. There is also one important piece of bad news about Redis that most guides get wrong.

What autoload means in wp_options

WordPress uses the wp_options table as a key-value store for site settings. Every row has an option_name, an option_value, and an autoload column. That autoload column is the whole story: it decides whether the row is pulled into PHP memory on every single request, or only when a plugin explicitly asks for it.

On every WordPress bootstrap, core calls wp_load_alloptions(), which runs one SQL query against wp_options to fetch every row whose autoload column matches "should autoload", serializes the entire result into a single blob, and stores it in the object cache under the key alloptions in the group options. From that moment on, every call to get_option() for an autoloaded option is answered from PHP memory. The benefit is that a handful of common options (siteurl, blogname, active_plugins, theme settings, and so on) are available instantly without 30 extra queries per page.

The cost is that every byte of autoloaded data enters PHP memory on every request, regardless of whether the current page needs it. An 8 MB autoload set means 8 MB is deserialized into a PHP array on every front-end page view, every admin screen, and every REST or AJAX call. That is the bloat pattern: a small, bounded idea turned into an unbounded one by years of plugin churn.

The WP 6.6 five-value autoload model (and why this matters for SQL)

Before WordPress 6.6, autoload had two values: yes and no. add_option() defaulted to yes, which meant that every plugin option you ever installed was autoloaded unless the plugin author explicitly opted out. Most did not.

WordPress 6.6 (released June 2024) replaced that binary with a five-value system, documented in the Make WordPress Core post "Options API: Disabling autoload for large options". The new values are:

Database value Meaning Autoloaded?
on Explicitly set to true by a developer Yes
off Explicitly set to false by a developer No
auto Added with null default, WordPress will decide later Yes, for now
auto-on Default value, dynamically resolved to autoload Yes
auto-off Default value, dynamically resolved to not autoload No

The add_option() and update_option() functions no longer default to yes. The default is now null, which tells WordPress to decide based on the size of the option. Anything larger than roughly 150 KB that is added with the default gets stored as auto-off, per the add_option() reference on developer.wordpress.org. The legacy yes and no values still work but were formally deprecated in WordPress 6.7 in favor of booleans and the new string values.

This has a direct consequence for anyone running "how to measure autoload size" queries copied from older guides. The classic query looks like this:

SELECT SUM(LENGTH(option_value))
FROM wp_options
WHERE autoload = 'yes';

On a site that was installed or has had rows added on WordPress 6.6 or later, that query undercounts, because it ignores on, auto, and auto-on. The correct, version-safe query is:

SELECT
  ROUND(SUM(LENGTH(option_value)) / 1024, 2) AS autoloaded_kb,
  COUNT(*)                                   AS autoloaded_count
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto');

The four values in that IN clause are what core itself checks. They come from wp_autoload_values_to_autoload(), the helper WP 6.6 introduced specifically so that any code that needs to ask "which rows does WordPress consider autoloaded" gets a single authoritative answer. If you see an older hosting script or StackOverflow answer using only autoload = 'yes', assume it is stale.

How WordPress uses autoloaded data on every request

A simplified version of the bootstrap sequence looks like this:

  1. WordPress starts, wp-settings.php runs, the database connection opens.
  2. Core calls wp_load_alloptions().
  3. wp_load_alloptions() checks the object cache for the key alloptions in group options. If present, it returns that array. If not, it runs one SQL query against wp_options with the four-value IN clause, stores the entire result as a single blob in the object cache, and returns.
  4. The rest of the request runs. Every get_option() for an autoloaded name is answered from memory. Only non-autoloaded options trigger fresh SQL queries.

The thing to internalize is that alloptions is one cache entry, one serialized blob, one memory allocation. It is not a per-option cache. It is a giant bucket, and WordPress either reads the whole bucket or none of it on every request. That design keeps the code simple, and for a reasonably sized site it is fast. It is also what makes bloat so expensive: there is no way to partially load half the bucket.

Measuring your autoloaded data correctly

The right question is not "how big is my wp_options table." A wp_options table with 50,000 rows but only 200 KB of autoloaded data is perfectly healthy, because only autoloaded rows touch the hot path. The right question is "how much data is autoloaded right now." Open phpMyAdmin from your hosting control panel (most hosts put it under "Databases"), select your WordPress database, click the SQL tab, and run the four-value query above. If your host offers Adminer instead of phpMyAdmin, the same query works there.

If you have SSH access, WP-CLI can run the same query from the command line:

wp db query "SELECT \
  ROUND(SUM(LENGTH(option_value)) / 1024, 2) AS autoloaded_kb, \
  COUNT(*) AS autoloaded_count \
  FROM wp_options \
  WHERE autoload IN ('yes', 'on', 'auto-on', 'auto');"

Rough size benchmarks to interpret the result:

Autoloaded size What this means
Under 300 KB Healthy. Stop optimizing this and look elsewhere.
300 KB to 800 KB Acceptable, worth monitoring.
800 KB to 1 MB WordPress Site Health flags this as a critical issue.
1 MB to 3 MB Measurable impact per request. Act.
More than 3 MB Significant overhead on every page view. Urgent.

The 800 KB figure is not arbitrary. WordPress 6.6 added a Site Health check for autoload size that marks any site over 800 KB of autoloaded data as having a critical performance issue. Older hosting guides still cite 1 MB as the threshold. That number has been out of date since mid-2024.

Once you know the total, find the worst individual rows:

SELECT
  option_name,
  ROUND(LENGTH(option_value) / 1024, 2) AS size_kb,
  autoload
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto')
ORDER BY LENGTH(option_value) DESC
LIMIT 20;

This is the list you will actually work from. In practice, a handful of rows usually account for 80 to 90 percent of the total.

The common offenders

Autoload bloat has the same five or six root causes on almost every site I have seen.

Leftover options from uninstalled plugins. A plugin activates, calls add_option('my_plugin_settings', $bigArray), and never cleans up. Months later the plugin is deleted. The row stays in wp_options forever, still autoloaded, still adding bytes to every request. On a site that has cycled through dozens of plugins over years, this is almost always the biggest contributor.

Transients bleeding into autoload. WordPress stores transients in wp_options when no persistent object cache is active. Expired transients are only deleted when something next requests them, which for orphaned plugins means never. The value row (_transient_<name>) tends to be autoloaded, while the timeout row (_transient_timeout_<name>) is not. Orphaned transients accumulate silently.

Plugins that store API responses and license payloads. Some plugins cache remote license-server responses, remote feature flags, or analytics payloads inside update_option(). A single row can be hundreds of kilobytes, and it is autoloaded on every request even though the admin screen only needs it once a day.

Customizer and theme data. theme_mods_<theme> rows can grow if the theme stores large amounts of customizer state. Page builders are particularly prone to this.

WooCommerce and ecommerce extensions. Session data, large product-attribute taxonomies, shipping-zone configurations, and various extension settings. On a WooCommerce site with 20 plugins, autoload creep can happen fast.

max_allowed_packet errors as a related symptom. If a plugin tries to save a very large value and you see Got a packet bigger than 'max_allowed_packet' bytes in the error log, that is a separate failure mode but the same root cause: something is using wp_options as a general-purpose data store. The fix is to not store huge payloads in wp_options in the first place. Raising the MySQL limit is the last resort, not the first.

The Redis misconception that trips up most sites

I want to spend a section on this because it is the single most common false belief I see on hosting tickets. The belief is that Redis "fixes" autoload problems. It does not.

Here is the actual mechanism. The alloptions cache entry is stored under a single Redis key. On a cold cache, WordPress queries the database for all autoloaded options, serializes the entire result, and stores it as one Redis value. On every subsequent request, WordPress pulls that one Redis value, deserializes it, and holds it in PHP memory for the request. The slow part of the original flow (the MySQL query) has been replaced by a faster part (a Redis GET). That is a real win, and it is why a properly configured Redis object cache helps WordPress performance in general.

But the PHP memory cost does not go away. If your autoloaded data is 8 MB, every request still pulls 8 MB out of Redis, runs unserialize() on it, and holds an 8 MB array in PHP memory until the request ends. Redis made the fetch faster. It did not make the data smaller, and the deserialize and memory cost is what actually hurts on a slow PHP process.

Then there is the Memcached failure mode, which is worse. Memcached has a default 1 MB per-key limit. WordPress VIP's documentation on autoloaded options notes that when alloptions exceeds 1 MB on a Memcached-backed cache, the cache set silently fails. WordPress then falls back to the MySQL query on every request, and VIP has specifically documented HTTP 503 responses tied to "Error 1024 (alloptions)" as the downstream symptom. The site becomes dramatically slower than it would be without object caching at all. If you are on Memcached and your autoload is approaching 1 MB, fix the data, do not pile more cache on top.

The correct mental model: Redis caches dirty data faster. Fix the data first, then enjoy the cache.

How to shrink autoloaded data safely

The idea most people have wrong is that touching wp_options is dangerous. The truth is nuanced. Disabling autoload on a row is almost always safe, because you are not changing the value, you are only telling WordPress to fetch it on demand instead of preloading it. If a plugin needs the option, get_option() will still find it, just via a fresh SQL query at call time. No functionality is lost. Deleting rows is the risky part, and you only do that once you are sure the option is orphaned.

The safe workflow is:

  1. Take a database backup. Every destructive step below assumes you have a rollback.
  2. Measure. Run the four-value SQL query. If you are under 800 KB, stop here. Your problem is somewhere else, and my article on what a slow database actually means covers the other four common causes.
  3. Identify offenders. Run the top-20 query above. Write down each large row and categorize it.
  4. Disable autoload on the easy wins first. For anything that is obviously safe to not preload on every request (large license-response blobs, infrequently read settings, old plugin data you are not sure about yet), flip it to off. Via WP-CLI: wp option set-autoload <option_name> off. This is the command documented on developer.wordpress.org/cli. Via SQL: UPDATE wp_options SET autoload = 'off' WHERE option_name = 'option_name_here';.
  5. Clean up expired transients. Transients cluster in wp_options and the expired ones are safe to delete. The easiest route is a maintained cleanup plugin such as WP-Optimize or Advanced Database Cleaner, which offer a one-click "delete expired transients" button in wp-admin. You can also run this SQL in phpMyAdmin: DELETE FROM wp_options WHERE option_name LIKE '_transient_timeout_%' AND option_value < UNIX_TIMESTAMP(); followed by DELETE FROM wp_options WHERE option_name LIKE '_transient_%' AND option_name NOT LIKE '_transient_timeout_%' AND option_name NOT IN (SELECT REPLACE(option_name, '_transient_timeout_', '_transient_') FROM (SELECT option_name FROM wp_options WHERE option_name LIKE '_transient_timeout_%') AS t);. WP-CLI alternative: wp transient delete --expired.
  6. Delete confirmed orphans. For rows belonging to plugins you have fully uninstalled, delete them. In phpMyAdmin, select the row in the wp_options table and click Delete, or run DELETE FROM wp_options WHERE option_name = 'option_name_here'; in the SQL tab. WP-CLI alternative: wp option delete <option_name>. Only do this for rows where you are sure the plugin is not coming back.
  7. Re-measure. Run the four-value query again. You want to be comfortably under 800 KB, ideally under 300 KB.

Rows you should never touch the autoload status on: siteurl, home, blogname, active_plugins, template, stylesheet, WPLANG, db_version, and any option whose name starts with wp_. These are core and need to be autoloaded. Same for anything your currently active plugins actively rely on for per-request logic. The whole point of autoload is that a small core set of options is fast. The goal is to get back to a small core set, not to eliminate autoload entirely.

Why WP 6.6 fresh installs are less likely to bloat

The 6.6 changes matter for the future, not the past. On a fresh install running 6.6 or newer, a plugin that calls add_option('my_big_payload', $bigArray) without passing an explicit autoload argument will get auto-off assigned automatically because the payload exceeds 150 KB. That is exactly the behavior you want.

But: existing rows in your database keep whatever autoload value they had when they were written. There is no automatic migration. A site that was installed in 2019 and has been upgraded through every release is still carrying the yes-heavy footprint of every plugin install it has seen. WP 6.6 saved you from tomorrow's bloat. Today's bloat you clean up by hand.

Preventing autoload bloat going forward

A few habits keep autoload under control on sites I manage:

  • Audit new plugins before installing. A quick look at add_option() calls in the plugin source file tells you whether it registers autoloaded options with explicit false or whether it relies on the default. Mature plugins are usually fine, but smaller or older ones are where the surprises live.
  • Run the four-value query every quarter. Make it a scheduled chore. Five minutes every three months catches creep before it becomes a critical Site Health warning.
  • Prefer plugins that register proper uninstall hooks. The Plugin Handbook section on uninstall.php documents how a plugin is supposed to clean up its options when deleted. Plugins that do not follow this pattern are the main source of orphaned rows.
  • Treat large autoloaded values as a code smell. If a plugin (especially a custom one) is storing 100 KB in a single wp_options row, that data probably belongs in a custom table, a transient with a long expiry on an object cache backend, or the filesystem. wp_options is not a cache; it is the settings table.
  • Keep the WordPress Performance plugin in mind. The Performance Lab plugin maintained by the WordPress Performance team ships with autoload-related health checks and other performance-leaning additions.

When to escalate

Call in help if you see any of the following:

  • Autoloaded data over 3 MB and no obvious single offender in the top-20 list.
  • Site Health flags autoload as critical even after a cleanup pass, because something is re-adding autoloaded rows on every request.
  • max_allowed_packet errors in the MySQL error log, which indicate a plugin writing massive payloads into wp_options.
  • HTTP 503 errors or intermittent cache misses after enabling Memcached on a large-autoload site, the documented WordPress VIP failure mode.
  • Unfamiliar option names dominating the top-20 list that you cannot attribute to any installed plugin.

When you reach out, have this ready: the output of the four-value measurement query, the top-20 offenders query, your WordPress and PHP versions, whether an object cache backend is active (Redis or Memcached) and how much memory it has, the list of currently active plugins, and a recent database backup. That is enough for someone to triage without a second round trip.

If autoload bloat is one symptom of a broader performance problem, the high TTFB article walks through the other common causes of server-side slowness on WordPress. If the broader issue is accumulated waste across the whole database (revisions, orphaned postmeta, expired transients, fragmented tables), the database cleanup guide covers proactive maintenance beyond wp_options.

Done chasing slowdowns?

Performance issues tend to come back after quick fixes. WordPress maintenance keeps updates, caching and limits consistent.

See WordPress maintenance

Search this site

Start typing to search, or browse the knowledge base and blog.