WordPress autoloaded options are the rows in wp_options that get pulled into memory on every single request, front end and admin alike, whether the page needs them or not. When Site Health calls them out, the fix is almost never “delete them” — it is to leave the data alone and turn autoload off for the handful of rows that are carrying the weight.
The number that matters is the total serialized size of everything marked for autoload. WordPress 6.6 added a Site Health test that goes critical past 800,000 bytes with the label “Autoloaded options could affect performance”. That threshold is a prompt to look, not a cliff edge: a 900 KB total made of one abandoned plugin’s cache row is a five-minute fix, while 700 KB spread evenly across forty active plugins may be nothing you can do much about.
What WordPress autoloaded options actually are
Every row in wp_options has an autoload column. On boot, wp_load_alloptions() runs a single query that selects option_name and option_value for every row whose autoload value is in the set returned by wp_autoload_values_to_autoload(), then caches the whole lot under the cache key alloptions in the options group. After that, get_option() for any of those names is an array lookup rather than a query.
So one query is not the cost. The cost is everything else: transferring the payload from MySQL, unserializing it into PHP memory, and — if you have a persistent object cache — fetching and unserializing that same blob from the cache on every request. A 2 MB alloptions array is 2 MB of work your site does before it renders a single character, repeated for every uncached hit.
WordPress 6.6 reworked what the column can hold, and because there is no upgrade routine, an older site will have a mix of old and new values sitting side by side:
| Value | Meaning | Autoloaded? |
|---|---|---|
on | Explicitly set to autoload by whoever wrote the option | Yes |
off | Explicitly set not to autoload | No |
auto | No explicit value given — WordPress decides | Yes, under current defaults |
auto-on | WordPress decided, dynamically, that it should autoload | Yes |
auto-off | WordPress decided it should not — typically because the value is large | No |
yes / no | Legacy values from before 6.6, deprecated in 6.7 but still honoured | yes only |
yes and no will persist on established sites indefinitely.The auto-off value is where the dynamic part lives. Since 6.6, an option added or updated without an explicit true for autoload is not autoloaded once its value passes roughly 150,000 bytes, which the wp_max_autoloaded_option_size filter controls. That is a guard rail for new data only. It does nothing about the row a plugin wrote in 2021 with an explicit yes, which is usually the row you are hunting.
Measure before you change anything
Three ways to get the number, in descending order of how much I trust them.
WP-CLI, if you have shell access
# Total bytes of everything currently autoloaded
wp option list --autoload=on --format=total_bytes
# The twenty heaviest autoloaded rows, largest last
wp option list --autoload=on --fields=option_name,size_bytes \
| sort -n -k 2 | tail -20
Note the sort pipe. wp option list --orderby accepts only option_id, option_name and option_value — there is no --orderby=size_bytes, and piping through sort -n is the approach WP-CLI’s own documentation uses. The --autoload=on filter matches autoloaded rows regardless of whether they say on, yes or auto, so you do not need to enumerate the values yourself.
SQL, if all you have is phpMyAdmin
-- Total autoloaded payload in kilobytes
SELECT ROUND( SUM( LENGTH( option_value ) ) / 1024, 1 ) AS autoload_kb
FROM wp_options
WHERE autoload IN ( 'yes', 'on', 'auto', 'auto-on' );
-- The worst offenders
SELECT option_name,
ROUND( LENGTH( option_value ) / 1024, 1 ) AS size_kb,
autoload
FROM wp_options
WHERE autoload IN ( 'yes', 'on', 'auto', 'auto-on' )
ORDER BY LENGTH( option_value ) DESC
LIMIT 25;
Swap wp_ for your own table prefix. Include all four values in that IN list — a query that checks only autoload = 'yes' will under-report on any site that has written options since 6.6, which is every site.
Site Health, if you have neither
Go to Tools → Site Health → Status. Under 800,000 bytes you get a passing result reading “Autoloaded options are acceptable”; over it, the test returns a critical status, the “Autoloaded options could affect performance” label, and the row count and total size. If you work on client sites, reading the rest of the Site Health panel properly is worth the ten minutes — this is one test among many that people scroll past.
Moving the threshold is legitimate, occasionally
site_status_autoloaded_options_size_limit, with companion filters site_status_autoloaded_options_limit_description and site_status_autoloaded_options_action_to_perform for the wording. Raising it is defensible on a large multisite where you have already audited the total and know it is legitimately 1.2 MB of real settings. Raising it so a client stops asking about a red box is how you end up at 4 MB two years later.What is usually taking up the space
Run the top-25 query and you will almost certainly recognise something from this list. The sizes are not the point — the pattern in the option name tells you what wrote it and therefore what is safe to do.
| What you see | Where it came from | What to do |
|---|---|---|
_transient_* rows with no matching _transient_timeout_* | set_transient() called with no expiry. Non-expiring transients are autoloaded by design; transients with an expiry are not | Leave the data, disable autoload. Deleting works too, but the plugin will simply rebuild it next request |
| Options prefixed for a plugin you removed months ago | Uninstall routines are optional, and most plugins do not write one | Delete, after you have confirmed the plugin folder is genuinely gone |
theme_mods_* for themes you no longer run | One row per theme ever activated, kept so switching back restores your settings | Disable autoload. Keep the rows if you might switch back |
rewrite_rules | Core, and it grows with every custom post type and taxonomy you register | Leave it alone entirely. It is autoloaded because almost every request needs it |
cron | Core’s scheduled-event array. Bloats when jobs pile up faster than they run | Do not touch autoload. Fix the queue instead — the size is a symptom |
| Licence checks, update payloads, remote API responses stored as options | Commercial plugins caching a vendor response and marking it autoload | Disable autoload. It is needed on an update screen, not on a product page |
| Multi-megabyte log or history arrays | A plugin using wp_options as a table it should have created | Disable autoload, then ask whether you want the plugin |
set_transient() source; the rest is from auditing production sites.One row deserves a special mention because it panics people: core_updater.lock. It is tiny, it is temporary, and it has nothing to do with this. If you found it while poking around and your updates are stuck, that is the “another update is currently in progress” problem, not an autoload problem.
How to disable autoload without corrupting the value
Since WordPress 6.4 there has been a function for exactly this, and it changes the autoload column without reading, re-encoding or rewriting option_value:
<?php
// One option. Returns true if the autoload value changed.
wp_set_option_autoload( 'some_plugin_cached_feed', false );
// Several at once. Returns an array keyed by option name.
wp_set_option_autoload_values( array(
'some_plugin_cached_feed' => false,
'some_plugin_license_check' => false,
'theme_mods_twentytwenty' => false,
) );
Pass booleans. The string values 'yes' and 'no' are still accepted but were deprecated in 6.7, so there is no reason to write new code with them.
From the shell, run that through wp eval rather than reaching for wp option update:
wp eval 'var_dump( wp_set_option_autoload( "some_plugin_cached_feed", false ) );'
Why not wp option update --autoload=off
wp option update does accept --autoload=on|off|yes|no, but it is a command for writing a value. Omit the value and it reads from STDIN, so the command appears to hang. Supply one with $(wp option get ...) and you have round-tripped a serialized array through plaintext, which is a real way to corrupt a settings row. wp_set_option_autoload() touches only the column you want changed.If you truly only have database access, the UPDATE is one line — but the object cache will keep serving the old alloptions array until it is cleared, so the change will look like it did nothing:
UPDATE wp_options SET autoload = 'off'
WHERE option_name = 'some_plugin_cached_feed';
-- then, from the shell:
-- wp cache flush
Take a database export first. Not a backup you took — one you have restored somewhere and looked at. And if a careless edit here does take the site down with a database error, the database connection error walkthrough and enabling debugging to read the log will get you further than guessing.
Disable autoload or delete the row?
These are different operations with different risk profiles, and conflating them is how people break sites while “optimising” them. Disabling autoload is reversible, costs you one extra query on the requests that genuinely need the option, and cannot lose data. Deleting is permanent and can lose settings a plugin has no way to rebuild.
Do not disable autoload on everything
wp_load_alloptions() has a fallback: if the autoload query returns no rows at all, it loads every option in the table instead. Strip autoload from the lot and you do not get a lean boot, you get the entire wp_options table in memory on every request — the exact opposite of the goal.Clear expired transients while you are in there
Expired transients are not autoloaded, so they do not show up in your autoload total. They still bloat the table, slow down the queries that scan it, and get copied into every backup you take. Two commands:
# Safe: removes only transients whose timeout has passed
wp transient delete --expired
# On multisite, site transients are stored separately
wp transient delete --expired --network
wp transient delete --all also exists and I would not run it on a busy production site mid-morning. Every plugin that was relying on a cached value now rebuilds it on the next request that needs it, which can mean a burst of remote HTTP calls and database work arriving all at once. Run it when traffic is low, or stick to --expired.
A persistent object cache changes this picture: with Redis or Memcached in place, transients are stored in the cache rather than in wp_options, and there is nothing in the table to clean. If you are not sure which you have, Site Health tells you under Tools → Site Health → Info → Server.
When autoload bloat is not your real problem
Being honest about this saves you an afternoon. Shaving 300 KB off alloptions is a real improvement to time-to-first-byte on uncached requests, measurable in tens of milliseconds on a decent host. It is not going to rescue a site that is slow for a different reason.
A slow admin is usually several things at once. Autoload bloat is one of the four causes I check, alongside the Heartbeat API hammering admin-ajax.php, a jammed WP-Cron queue, and a single plugin running expensive queries on every admin screen. Diagnosing a slow WordPress admin dashboard works through all four in order, which is the right sequence if your complaint is “wp-admin is unusable” rather than “Site Health has a red box”.
It will not move your Core Web Vitals much. Autoload cost is server-side work that lands in TTFB. If your problem is interaction latency, the bottleneck is JavaScript on the main thread, and diagnosing a failing INP score is where to spend the time instead. If page caching is serving most of your traffic, the front end barely pays the autoload cost at all — but every logged-in request and every uncacheable page still does.
There is one way this gets genuinely bad. Memcached’s default maximum item size is 1 MB, set by -I / --max-item-size. An alloptions array larger than that cannot be stored, so the set silently fails, every request misses the cache, and your “optimised” site queries and unserializes the whole thing from MySQL every time — while the dashboard cheerfully reports a persistent object cache in use. Crossing 1 MB of autoloaded options on a Memcached host is not a slow degradation, it is a step change. Redis does not have the same low ceiling, but the unserialization cost is still real.
If one plugin owns most of the total, the plugin is the decision. I have seen a single slider plugin account for 1.4 MB of autoloaded options. You can disable autoload on its rows every quarter, or you can replace it. Work out which plugin is responsible first — the step-by-step plugin conflict process is the same deactivate-and-measure discipline applied to a different symptom.
A repeatable 15-minute audit
- Export the database, and confirm the export is restorable.
- Record the starting number:
wp option list --autoload=on --format=total_bytes. - List the top 20 rows and write down which plugin owns each one. Anything you cannot attribute, leave alone for now.
- Delete only rows belonging to plugins that are no longer installed.
- For everything else over about 50 KB that is clearly admin-only, call
wp_set_option_autoload_values()once with the whole batch. wp transient delete --expired, thenwp cache flush.- Re-run the total, then click through the front end, the editor, and the settings screen of every plugin you touched. Autoload changes break things silently, not loudly.
- Note the date and the new figure somewhere you will find it. This grows back.
Step 7 is the one people skip and the only one that catches a mistake. A plugin whose licence row you set to off may read that option in a context where an extra query is fine, or it may have been written on the assumption that the value is always already in memory. You find out by looking, not by reasoning about it.
Frequently asked questions
What is a good size for autoloaded options in WordPress?
Core’s Site Health test passes below 800,000 bytes and reports a critical status above it, so that is the number to aim under. In practice a lean site sits between 200 KB and 500 KB, a site with thirty active plugins will be higher, and anything approaching 1 MB deserves an audit — particularly on a Memcached host, where 1 MB is the default item size ceiling.
Is it safe to set autoload to off for an option?
Usually, and it is reversible either way — the value is untouched, so get_option() still returns it, just with one extra query. The risk is not data loss but behaviour: code written on the assumption that an option is always in memory can perform worse, or trip over an unexpected query during an early hook. Change the heavy rows, test the affected screens, and keep a record of what you changed.
Why did my autoloaded options total go back up?
Because a plugin wrote the option again. update_option() on an existing row does not reset autoload, but a plugin that deletes and re-adds its row — which many do on update or on settings save — gets core’s default autoload behaviour back. If a specific option keeps returning, that is a bug report for the plugin author, not something you can fix permanently from the database.
Do I need a plugin to clean up autoloaded options?
No. Everything here is core functions, two WP-CLI commands and a SELECT statement. A database optimisation plugin can show you the same list in a nicer table, which is genuinely useful if you have no shell access, but installing a plugin that runs on every request to inspect a per-request cost is a trade worth thinking about. Whatever you use, make the judgement call about each row yourself — no tool knows which of your options are admin-only.
Does a persistent object cache make autoloaded options irrelevant?
It moves the cost rather than removing it. The MySQL query goes away, but the alloptions blob still has to be fetched from Redis or Memcached and unserialized into PHP memory on every request. And on Memcached, an array over the 1 MB default item limit cannot be cached at all, which turns a cache into a per-request database read with extra steps.
Why does my autoload column say yes instead of on?
Because the row predates WordPress 6.6 and core deliberately shipped no upgrade routine for the column. yes and no are still honoured as on and off, so a mixed table is normal and not a sign of corruption. It only matters when you write SQL by hand — check for all four autoloading values, not just yes.
Sources
- Options API: Disabling autoload for large options on make.wordpress.org — the autoload column values, the 150,000-byte default and the
wp_max_autoloaded_option_sizefilter WP_Site_Health::get_test_autoloaded_options()— the 800,000-byte threshold, the critical status and label, and the three filterswp_load_alloptions()— thealloptionscache key, theoptionsgroup, and the load-everything fallbackwp_set_option_autoload()andwp_set_option_autoload_values()— added in 6.4.0, string values deprecated in 6.7.0set_transient()— source for non-expiring transients being autoloaded and expiring ones notwp option list,wp option updateandwp transient deletein the WP-CLI command reference- memcached(1) manual page — the 1 MB default for
-I/--max-item-size

Leave a Reply to This Post