The Query That Runs Before Anything Else
Before WordPress renders a single pixel - before your theme loads, before plugins initialize, before any of your code runs - it executes this:
SELECT option_name, option_value FROM wp_options WHERE autoload IN ('yes', 'on', 'auto', 'auto-on')
Every page load. Every admin request. Every AJAX call. Every REST API hit. Every WP-Cron tick. There is no way to skip it, no filter to bypass it. It's hardcoded into wp_load_alloptions() and it runs before WordPress even knows what page you're requesting.
On a fresh WordPress install, this query returns around 150-200 rows totaling maybe 100KB. That's fine - it takes a millisecond or two.
On a 2-year-old WooCommerce site with 30+ plugins installed (and 15 of them later deleted), I regularly see 2,000-4,000 rows totaling 3-8MB. I've seen sites with 40MB+ of autoloaded data that were essentially crashing on every request. That's your backend performance quietly dying.
What the Numbers Actually Look Like
I've been doing WordPress performance work for 15 years. Over the last few years I started tracking autoload sizes across client sites more carefully. Here's what I found across 50+ sites I've optimized:
| Site Type | Avg Autoload Size | Avg Row Count | Worst I've Seen |
|---|---|---|---|
| Fresh install (no plugins) | 85KB | 140 rows | - |
| Simple blog (5-8 plugins) | 350KB | 400 rows | 900KB |
| Business site (15-20 plugins) | 1.8MB | 1,200 rows | 5.2MB |
| WooCommerce store | 3.4MB | 2,800 rows | 12MB |
| Multisite (per site) | 2.1MB | 1,600 rows | 8MB |
| Site with page builder history | 4.8MB | 3,200 rows | 18MB |
| Average across all sites | 4.2MB | 2,100 rows | 41MB |
That average of 4.2MB means WordPress is loading over 4 megabytes of serialized data into PHP memory on every single request before it even starts doing anything useful. On shared hosting with slow disks, that query alone can take 200-800ms. On a site getting 50 requests per second, you're reading and parsing gigabytes of data per minute for no reason.
The page builder category is the worst offender and it's not even close. A site that used Elementor for two years, then switched to Gutenberg, can easily have 2-4MB of orphaned Elementor data still autoloading on every request. The old builder's CSS cache, template data, global settings - all sitting there in wp_options with autoload set to 'yes', doing absolutely nothing except wasting memory and CPU.
Why WordPress Loads Everything Upfront
Here's what's going on under the hood. When WordPress boots, it calls wp_load_alloptions(). This function runs one big SELECT query to grab every option flagged for autoloading, then stores the whole result in the object cache under the key alloptions.
The idea is actually smart: instead of running 200 individual SELECT * FROM wp_options WHERE option_name = 'X' queries as each plugin calls get_option(), WordPress batches them all into one query upfront. One database round-trip instead of 200. That's a huge win - when the total size is reasonable.
The problem is that this approach has no size limit. WordPress doesn't care if alloptions is 100KB or 40MB - it loads the whole thing. And once it's in memory, every call to get_option() for an autoloaded option is a fast array lookup. But the initial load cost scales linearly with the total size of autoloaded data.
// This is essentially what wp_load_alloptions() does (simplified)
function wp_load_alloptions() {
$alloptions = wp_cache_get( 'alloptions', 'options' );
if ( ! $alloptions ) {
// This is THE query - one big fetch
$alloptions_db = $wpdb->get_results(
"SELECT option_name, option_value
FROM $wpdb->options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on')"
);
// Store everything in memory
foreach ( $alloptions_db as $o ) {
$alloptions[ $o->option_name ] = $o->option_value;
}
wp_cache_set( 'alloptions', $alloptions, 'options' );
}
return $alloptions;
}
Notice that WHERE clause. If you're running WordPress 6.6+, it's not just checking for 'yes' anymore - more on that in a minute.
The Object Cache Factor
If you're running Redis or Memcached as a persistent object cache, you might think autoload bloat doesn't matter. And it's true that with object cache, WordPress doesn't hit MySQL on every request - it pulls alloptions from Redis instead.
But object cache doesn't make the problem disappear. It shifts it.
A 5MB alloptions blob still needs to be:
- Deserialized from Redis on every request (Redis stores it serialized)
- Loaded into PHP memory - your PHP process still needs that 5MB+ in RAM
- Kept in Redis memory - bloated alloptions eats into your Redis allocation
I worked on a WooCommerce site with 8MB of autoloaded data and Redis object cache. The site owner thought performance was fine because MySQL queries were fast. But PHP memory usage was through the roof - each request was consuming 40MB+ of RAM because of the alloptions deserialization overhead. On a server handling 100 concurrent requests, that's 4GB of RAM just for autoloaded options that nobody needs.
Object cache helps with the database query time. It does nothing for the memory and deserialization cost. If your autoload size is bloated, you need to fix the source, not add another cache layer. This is the same reason caching plugins don't fix slow WordPress - they mask symptoms, they don't fix root causes.
How It Gets Out of Control
There are four ways autoload bloat accumulates, and most sites have all four:
1. Plugins That Autoload Everything
When a plugin calls add_option('my_setting', $value), WordPress sets autoload to 'yes' by default. Most plugin developers never think about this. A contact form plugin that stores its configuration, email templates, submission logs, and license key all as separate autoloaded options - even though 90% of that data is only needed on the admin settings page.
I've seen a single SEO plugin create 47 autoloaded options totaling 1.2MB. On a site with 500 pages, the plugin was storing serialized metadata for every page in a single option. That option alone was 800KB, loading on every frontend request even though it was only used when generating sitemaps.
2. Deleted Plugins Leave Data Behind
This is the big one. When you deactivate and delete a plugin through the WordPress admin, WordPress removes the plugin files but does NOT remove its database entries. The plugin's uninstall.php or register_uninstall_hook is supposed to clean up - but most plugins either don't have one, or don't clean everything.
I audited a site that had used 45 different plugins over three years. Only 18 were still active. The wp_options table had 1,847 rows from the 27 deleted plugins, all still autoloading. That was 6.3MB of completely dead data loading on every request.
3. Transients Without Expiry
Some plugins store transient data in wp_options (which is where transients go when there's no object cache) and set autoload to 'yes' without setting an expiration. The data just sits there forever, loading on every request.
Even plugins that DO set an expiry often don't clean up after themselves. WordPress only deletes expired transients lazily - when something requests that specific transient. If nobody requests it, it stays in the table indefinitely. I've found _site_transient_ entries from 2019 still autoloading on sites in 2026.
4. Serialized Data Blobs
The worst offenders are single options that contain massive serialized arrays. One option_name, one row in the database, but the option_value is 500KB-2MB of serialized PHP data. Common culprits:
- Page builder global CSS/settings cache
- Plugin migration/update logs from three years ago
- Cached API responses that never expire
- Widget configurations from themes you haven't used in years
- WPML string translation tables
-- Find the biggest individual options
SELECT option_name, LENGTH(option_value) as size_bytes,
ROUND(LENGTH(option_value) / 1024, 1) as size_kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on')
ORDER BY size_bytes DESC
LIMIT 20;
On one site, the top single option was _elementor_global_css at 2.1MB. The site hadn't used Elementor in 14 months.
WordPress 6.6 Changed the Autoload System
If you're running WordPress 6.6 or later, the autoload column works differently than it used to. Instead of just 'yes' and 'no', there are now seven possible values:
| Value | Meaning | Autoloads? |
|---|---|---|
yes |
Legacy - explicitly autoloaded (pre-6.6 options) | Yes |
on |
Explicitly set to autoload | Yes |
auto |
No explicit preference - WordPress decides | Yes (by default) |
auto-on |
WordPress dynamically determined: should autoload | Yes |
auto-off |
WordPress dynamically determined: should not autoload | No |
no |
Legacy - explicitly not autoloaded | No |
off |
Explicitly set to not autoload | No |
The big change: add_option() now defaults to null instead of 'yes', which stores as 'auto'. This lets WordPress potentially make smarter decisions about what to autoload in the future.
But here's the thing - all your existing options from before the 6.6 upgrade still have 'yes' or 'no'. There's no migration. And options with 'auto' currently still autoload by default. So in practice, this change doesn't help with existing bloat at all. It's forward-looking infrastructure.
Your SQL queries need to account for this now:
-- Check total autoload size (WordPress 6.6+ compatible)
SELECT SUM(LENGTH(option_value)) as total_bytes,
ROUND(SUM(LENGTH(option_value)) / 1024 / 1024, 2) as total_mb,
COUNT(*) as option_count
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on');
How to Diagnose Your Autoload Bloat
Method 1: SQL Query (phpMyAdmin or WP-CLI)
The fastest way to check:
SELECT SUM(LENGTH(option_value)) as autoload_size
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on');
| Size | Status | Action |
|---|---|---|
| Under 500KB | Healthy | No action needed |
| 500KB - 1MB | Getting heavy | Worth a review |
| 1MB - 3MB | Slowing you down | Clean up soon |
| 3MB - 5MB | Real problem | Clean up now |
| Over 5MB | Emergency | Fix immediately |
Method 2: WP-CLI (Recommended)
If you have WP-CLI access, this is cleaner:
# Total autoload size
wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024, 2) as autoload_mb FROM $(wp db prefix)options WHERE autoload IN ('yes','on','auto','auto-on');" --skip-column-names
# Top 20 biggest autoloaded options
wp db query "SELECT option_name, ROUND(LENGTH(option_value)/1024, 1) as size_kb FROM $(wp db prefix)options WHERE autoload IN ('yes','on','auto','auto-on') ORDER BY LENGTH(option_value) DESC LIMIT 20;"
# Count by prefix (shows which plugins are the worst offenders)
wp db query "SELECT SUBSTRING_INDEX(option_name, '_', 2) as prefix, COUNT(*) as count, ROUND(SUM(LENGTH(option_value))/1024, 1) as total_kb FROM $(wp db prefix)options WHERE autoload IN ('yes','on','auto','auto-on') GROUP BY prefix ORDER BY total_kb DESC LIMIT 20;"
That last query is the most useful one. It groups options by their prefix (which usually maps to a plugin) and shows you exactly which plugins are dumping the most data into autoload. You can spot deleted plugins instantly - if you see a prefix for a plugin that's not installed, that's orphaned data.
Method 3: WordPress Site Health
WordPress 6.6+ added an autoload check to Site Health. Go to Tools → Site Health and look for a notice about autoloaded data size. It warns you if it's over 800KB. But it just tells you there's a problem - it doesn't tell you which options are causing it or what to do about it.
Method 4: Query Monitor Plugin
If you have the Query Monitor plugin installed, it shows you the alloptions data under the "Cache" panel. You can see the total size and individual options. Good for development sites, but I wouldn't install it on production just for this.
The Fix: Step by Step
Step 1: Identify What's Active vs Orphaned
Before changing anything, you need to know what's currently active. Get a list of your active plugins:
wp plugin list --status=active --field=name
Then compare against your autoloaded option prefixes. Options from plugins NOT in that list are candidates for cleanup.
# List all unique option prefixes that are autoloaded
wp db query "SELECT DISTINCT SUBSTRING_INDEX(option_name, '_', 2) as prefix FROM $(wp db prefix)options WHERE autoload IN ('yes','on','auto','auto-on') ORDER BY prefix;" --skip-column-names
Go through the prefixes manually. If you see elementor_, jetpack_, wordfence_ or whatever for plugins you removed years ago - those are safe to deal with.
Step 2: Disable Autoload for Orphaned Options
Don't delete options right away. Start by just turning off autoload:
-- Turn off autoload for a deleted plugin's options
UPDATE wp_options SET autoload = 'no'
WHERE option_name LIKE 'deleted_plugin_prefix_%'
AND autoload IN ('yes', 'on', 'auto', 'auto-on');
Or with WP-CLI:
# Safer: update one at a time and verify
wp option list --search='deleted_plugin_prefix_*' --autoload=on --format=table
# If the list looks right, disable autoload
wp option list --search='deleted_plugin_prefix_*' --autoload=on --field=option_name | while read opt; do wp option set-autoload "$opt" off; done
Important: Only target options from plugins that are NOT installed anymore. Changing autoload on active plugin options can break things in weird ways - the plugin expects its settings to be in memory but they're not, so it falls back to defaults or throws errors.
Verify after each change:
# Check that autoload size decreased
wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024, 2) as autoload_mb FROM $(wp db prefix)options WHERE autoload IN ('yes','on','auto','auto-on');" --skip-column-names
# Load the site and check for errors
wp eval 'echo "Site loads OK\n";'
curl -s -o /dev/null -w "%{http_code}" https://yoursite.com
Step 3: Clean Up Expired Transients
Expired transients are free wins - they're data that WordPress itself considers expired but hasn't bothered to delete:
# Delete expired transients (WP-CLI has a built-in command for this)
wp transient delete --expired
# Check how many site transients are expired too
wp transient delete --expired --network
If you don't have WP-CLI:
-- Delete expired transient timeouts and their data rows
DELETE a, b FROM wp_options a
INNER JOIN wp_options b
ON b.option_name = CONCAT('_transient_', SUBSTRING(a.option_name, 20))
WHERE a.option_name LIKE '_transient_timeout_%'
AND a.option_value < UNIX_TIMESTAMP();
-- Same for site transients (prefix is longer: '_site_transient_timeout_' = 25 chars)
DELETE a, b FROM wp_options a
INNER JOIN wp_options b
ON b.option_name = CONCAT('_site_transient_', SUBSTRING(a.option_name, 25))
WHERE a.option_name LIKE '_site_transient_timeout_%'
AND a.option_value < UNIX_TIMESTAMP();
Verify:
# Count remaining transients
wp db query "SELECT COUNT(*) as transient_count FROM $(wp db prefix)options WHERE option_name LIKE '%_transient_%';" --skip-column-names
Step 4: Deal With the Big Blobs
Check for individual options over 100KB:
wp db query "SELECT option_name, ROUND(LENGTH(option_value)/1024, 1) as size_kb FROM $(wp db prefix)options WHERE autoload IN ('yes','on','auto','auto-on') AND LENGTH(option_value) > 102400 ORDER BY LENGTH(option_value) DESC;"
For each big option, decide:
-
Active plugin, not needed on every request → set autoload to 'no' (the plugin will still load it on demand via
get_option(), just with an individual query) -
Deleted plugin → safe to delete entirely with
wp option delete option_name - Core WordPress → leave it alone
A common question: "If I set autoload to 'no' for an active plugin's option, will it break?" Usually no - it just means WordPress will run a separate query for that option when the plugin requests it. For a 500KB blob that's only used on admin pages, that's a much better tradeoff than loading it on every frontend request. But test it.
Step 5: Clear Object Cache After Changes
If you're running Redis or Memcached, the alloptions key in your object cache is now stale:
wp cache delete alloptions options
# Or flush the entire object cache
wp cache flush
If you skip this, WordPress will keep serving the old cached alloptions (with all the bloat) until the cache expires naturally.
Step 6: Measure the Impact
Before and after, measure your TTFB (Time To First Byte):
# Quick TTFB check (run 5 times and average)
for i in {1..5}; do
curl -s -o /dev/null -w "TTFB: %{time_starttransfer}s\n" https://yoursite.com
done
On the WooCommerce site I mentioned earlier (8MB autoload, Redis cache), after cleanup we got it down to 380KB. TTFB dropped from 1.2s to 0.4s. PHP memory per request dropped from 42MB to 18MB. The server went from struggling at 50 concurrent users to handling 200+ comfortably.
Preventing Future Bloat
Fixing autoload bloat once is great. But it comes back. Every new plugin you try, every plugin you delete, every transient that doesn't expire - it accumulates. Here's how to keep it under control:
Monitor Monthly
Set a calendar reminder or, better, automate it:
# Add to a monthly cron or monitoring script
AUTOLOAD_SIZE=$(wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024, 2) FROM $(wp db prefix)options WHERE autoload IN ('yes','on','auto','auto-on');" --skip-column-names 2>/dev/null)
echo "Autoload size: ${AUTOLOAD_SIZE}MB"
If it's growing more than 100KB per month, something is writing data it shouldn't.
Audit After Every Plugin Removal
When you deactivate and delete a plugin, immediately check what it left behind:
# After removing "example-plugin"
wp option list --search='example_plugin_*' --format=table
wp option list --search='example-plugin*' --format=table
Delete or de-autoload anything it left.
Run a Free Scan
If you're not sure where to start, run a free scan on your site. It checks autoload size along with other slow query patterns and gives you a prioritized list of what to fix.
Or Just Automate the Whole Thing
I built WP Multitool's Autoload Optimizer because I was tired of running these same SQL queries on every client site. It shows you every autoloaded option sorted by size, identifies which plugin each option belongs to (even deleted ones), and lets you toggle autoload on/off with one click. The Database Optimizer handles transient cleanup and orphaned data removal automatically.
It's the same thing I described above, just without the manual SQL work. One tool instead of copy-pasting queries from a notes file - that's the whole idea behind replacing multiple plugins with one.
The Bottom Line
Your WordPress site isn't slow because it's old. It's slow because every plugin you've ever installed left data in wp_options with autoload='yes', and nobody cleaned it up. The fix takes 20 minutes if you follow the steps above, and the results are usually dramatic - I'm talking 50-80% TTFB improvements on sites with serious bloat.
Check your autoload size right now. If it's over 1MB, you have work to do. If it's over 5MB, you're leaving significant performance on the table. And if caching isn't helping, this is almost certainly why - the autoload bloat happens before any cache layer kicks in.




Top comments (0)