Skip to content

What Is Autoload in WordPress? How a Bloated wp_options Table Weighs Down Every Request

Besides posts and pages, every WordPress database has a table called wp_options. It stores site-wide settings: the site title, the permalink structure, the list of active plugins, and the configuration of each plugin, one row per setting.

That table has a column named autoload. It looks like a simple flag, but any row with autoload enabled is loaded on every single request, whether that request needs it or not. When a long-running site’s admin screens gradually get heavier, a bloated autoload set is one of the first things worth checking.

Following the previous article on database index basics, this post uses measurements taken on this blog’s own WordPress database (7.1 series), using read-only queries only, to explain how autoload works and what happens when it grows too large.

wp_options: the home of site-wide settings

wp_options is a simple key-value table: option_name, option_value, and an autoload column. On this blog it holds 215 rows. Core, themes and plugins all read and write it through get_option() and update_option(), and core settings live side by side with plugin settings.

Autoload means “read it all up front”

Early in each request, WordPress calls wp_load_alloptions(), which fetches every autoloaded row in a single query and keeps the result in memory. After that, any get_option() call for an autoloaded option is answered from memory without touching the database. Options with autoload disabled are fetched individually, only when something actually asks for them.

Autoload enabled Autoload disabled
When it is read Early in every request, all at once Only when requested
Queries One query for all of them One query per option used
Cost on requests that do not use it Loaded anyway None

That design makes sense for values nearly every page needs. The catch is the last row: an autoloaded setting used by one obscure admin screen, or left behind by a plugin you deleted years ago, is still read on every page load.

Measuring this blog’s autoload set

Grouping this blog’s wp_options by the autoload value gives the following:

SELECT autoload, COUNT(*) AS cnt, SUM(LENGTH(option_value)) AS bytes
FROM wp_options GROUP BY autoload;
autoload value Rows Total value bytes Loaded up front?
on 101 38,669 Yes
auto 84 10,063 Yes
yes 3 40 Yes
off 24 6,022 No
no 3 3 No

188 of 215 rows, about 48 KB of values, are loaded up front. Counting the result of wp_load_alloptions() via wp eval gives the same 188. For a small blog like this one, that is nothing to worry about.

Why there are more values than yes and no

WordPress 6.6 reworked autoload. Besides the legacy yes/no (still honored), you will now see on/off (explicitly set), auto (left for WordPress to decide), and auto-on/auto-off (decided automatically). Which values count as autoloaded is defined by wp_autoload_values_to_autoload(); on this blog it returns yes, on, auto-on and auto.

Since 6.6, if an option is saved without an explicit autoload choice and its serialized size exceeds 150,000 bytes (the default of the wp_max_autoloaded_option_size filter), WordPress keeps it out of the up-front load. This does not help when a plugin explicitly asks for autoload, so it cannot prevent bloat on its own.

The largest rows show why they are needed every time

option_name Bytes What it is
_transient_wp_core_block_css_files 22,266 Cached list of core block CSS files
rewrite_rules 8,349 Rules that map URLs to content
wp_user_roles 3,177 Role and capability definitions
cron 2,748 Scheduled WP-Cron events

Core’s largest entries each have a clear reason to be loaded on every request: routing, permission checks, and the WP-Cron due-event check (see crontab syntax basics vs WP-Cron).

The top entry is a transient, stored in the same table (see how the WordPress Transient API works). In core’s set_transient(), a transient saved with an expiration is stored with autoload disabled, while one saved without an expiration is autoloaded:

$autoload = true;
if ( $expiration ) {
    $autoload = false;
    // the expiry time goes into a separate _transient_timeout_ row
}
add_option( $transient_option, $value, '', $autoload );

So any code that saves large transients without an expiration keeps adding to the up-front load.

The up-front query is a full table scan

Running EXPLAIN on the autoload query:

EXPLAIN SELECT option_name, option_value FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto');
type possible_keys key rows
ALL autoload NULL 215

An index on autoload exists, yet the optimizer chooses a full scan, because 188 of 215 rows match. This is the “most rows match, so scanning is cheaper” case from the previous article. The takeaway: autoload cost is about how much is read, not how it is found. No index fixes it; only reducing the autoloaded data does.

What gets heavier when autoload bloats

  • Every uncached request. Page caching bypasses PHP for anonymous visitors, but admin screens, logged-in browsing, REST API calls, WP-Cron runs, and the first hit after a cache expires all pay the cost. That is why “only the admin feels heavy” often points here.
  • PHP memory. The loaded values stay in memory for the whole request, and serialized arrays expand when unserialized, pushing requests closer to WP_MEMORY_LIMIT.
  • Persistent object caches. With Redis or Memcached, the autoload set is stored as one large alloptions item that is fetched and unserialized on each request. Some caches also cap the size of a single item; if the set outgrows that cap, it cannot be cached and falls back to the database every time.

Common causes

  • Settings left behind by deleted plugins that have no uninstall cleanup
  • Transients saved without an expiration
  • Plugins that keep appending logs or statistics to a single option
  • Large settings arrays, mostly used on one screen, stored as one autoloaded option

None of these break anything on a given day. They accumulate over years of operation.

The Site Health threshold

Tools → Site Health checks the total autoload size. In core, the warning fires above 800,000 bytes (the default of site_status_autoloaded_options_size_limit). Treat it as a prompt to look, not a hard line; the point where it actually hurts depends on the server and caching setup.

Checking without changing anything

# Total bytes of autoloaded options (WP-CLI)
wp option list --autoload=on --format=total_bytes

# Autoload value of one option
wp option get-autoload <option_name>

For a size-ordered list, run a SELECT like the one above through wp db query. Look at the total, the names of the largest rows (their prefixes usually hint at the owner), and whether that owner is still in use.

Fixing it safely: read, verify, keep a way back

  1. Back up first, so any change to wp_options can be reverted (see the 3-2-1 backup rule).
  2. Identify the owner. Never delete an option based on an unfamiliar name alone, and leave settings of active plugins alone.
  3. Prefer turning autoload off over deleting. wp option set-autoload <option_name> off keeps the value and simply loads it on demand, which keeps the risk of breaking something low.
  4. Handle transients with transient tools, such as wp transient delete --expired.
  5. Verify afterwards: admin screens, key pages, and the error log.

Note that wp db optimize defragments tables but does not shrink the autoload set (see wp db check and wp db optimize).

Pitfalls

  • Deleting options just because the names look unfamiliar
  • Turning off autoload for values used on every request, which adds a query each time
  • Expecting an index to help, when the cost comes from the volume read
  • Judging only by cached front-end pages while admin, REST and cron requests carry the load
  • Cleaning up once and never checking again
  • Assuming a deleted plugin took its settings with it

Summary

Autoload is a sensible way to load frequently used settings in one query, but every autoloaded row is read on every request, needed or not. On this blog, 188 of 215 rows (about 48 KB) are loaded up front via a full table scan, and what matters is the volume, not the lookup. Settings from removed plugins and transients without expirations pile up over time, so check the total and the largest rows periodically, and when you act, go in order: back up, identify the owner, prefer disabling autoload over deleting, then verify. Next time: revisions and auto-drafts, another thing that quietly piles up in the WordPress database.