The Options API
Reading and writing options, what autoload costs you, and the serialization WordPress does on your behalf.
get_option( string $option, mixed $default_value = false ): mixedOptions are WordPress's key/value store for site-wide settings: one row in wp_options per option, keyed by option_name, a varchar(191) unique column. Four functions cover it.
The four functions
get_option( $option, $default_value = false ) reads, returning false when the option does not exist and no default was given. add_option() creates only if absent. update_option() writes, creating the row if it is missing — so it is the shorthand for the usual add-or-update dance, and the longhand with add_option() is only worth writing when you must know which happened. delete_option() removes the row.
Serialization
Arrays and objects are serialized on write and unserialized on read; you never call serialize() yourself and you must never store JSON that you then decode by hand. Everything else comes back as a string, so cast integers and booleans on read. Prefer one option holding a settings array over twenty options.
Autoload
The autoload column decides whether an option is fetched in the single bulk query that runs on every request. Settings a page might need: yes. A cached API payload: no — pass false as the third argument. Since WordPress 6.6, when you pass null, values over 150,000 bytes are stored not-autoloaded by default (filter wp_max_autoloaded_option_size). To change autoload without touching the value, use wp_set_option_autoload(), added in 6.4.
Example
// Shorthand: update_option() creates the row if it does not exist yet,
// so this one call covers both insert and update.
update_option(
'acme_settings',
array(
'api_key' => $key,
'interval' => 300,
'enabled' => true,
),
false // Do not autoload - only the settings screen reads this.
);
// Longhand. Only worth writing when "did it already exist?" matters,
// e.g. seeding defaults on activation without clobbering a live config.
if ( false === get_option( 'acme_settings', false ) ) {
// add_option()'s third parameter is deprecated; pass '' and use the fourth.
add_option( 'acme_settings', $defaults, '', false );
}
// Arrays come back as arrays - WordPress unserializes on read.
// Everything else comes back as a string, so cast what you use.
$settings = get_option( 'acme_settings', array() );
$interval = (int) ( $settings['interval'] ?? 300 );
$enabled = ! empty( $settings['enabled'] );
// Returns false if the value was unchanged. That is not a failure.
$saved = update_option( 'acme_settings', $settings );
delete_option( 'acme_settings' );When to use it
- Storing a plugin's configuration in a single row and reading it with defaults merged in.
- Recording a schema or migration version so an update knows what work is outstanding.
- Cutting time-to-first-byte by moving a large cached payload out of autoload.
- Detecting first activation so defaults are seeded exactly once.
More examples
Auditing and fixing autoload
Run the query on any site with a slow time-to-first-byte. A multi-megabyte alloptions payload is fetched and unserialized before a single line of your theme runs, and it is usually one plugin's forgotten cache blob.
global $wpdb;
// What is being read on every single request, heaviest first?
// WordPress 6.6 added 'on', 'off', 'auto', 'auto-on' and 'auto-off'
// alongside the historic 'yes'/'no', so match all the truthy values.
$heaviest = $wpdb->get_results(
"SELECT option_name, LENGTH(option_value) AS bytes
FROM {$wpdb->options}
WHERE autoload IN ('yes','on','auto-on','auto')
ORDER BY bytes DESC
LIMIT 20"
);
// WordPress 6.4+: flip autoload without rewriting the value.
wp_set_option_autoload( 'acme_big_cache', false );
// Before 6.4 the only route was a re-save, which also bumped the value.
update_option( 'acme_big_cache', get_option( 'acme_big_cache' ), false );
// Raise or lower the 6.6 size threshold (default 150000 bytes).
add_filter( 'wp_max_autoloaded_option_size', function ( $max, $option ) {
return str_starts_with( $option, 'acme_' ) ? 50000 : $max;
}, 10, 2 );Telling "not set" apart from a stored false
The pattern to reach for whenever an option's absence means something different from its value — first-run detection, migration flags, opt-outs. Storing scalars rather than booleans avoids the problem entirely.
// get_option() returns false for a missing option AND for an option whose
// stored value is false, so a plain false check cannot distinguish them.
$flag = get_option( 'acme_onboarding_done' );
if ( false === $flag ) {
// Never set? Or explicitly set to false? No way to know.
}
// Pass a sentinel default instead.
$sentinel = '__acme_unset__';
$flag = get_option( 'acme_onboarding_done', $sentinel );
if ( $sentinel === $flag ) {
add_option( 'acme_onboarding_done', 0, '', true ); // First run.
} elseif ( ! $flag ) {
// Genuinely stored as falsy.
}
// Simpler: never store booleans. Store 1 and 0, or 'yes' and 'no', and
// the ambiguity disappears along with the string-casting surprises.
update_option( 'acme_onboarding_done', 1, true );One option holding a settings array
The structure the Settings API expects and the one that keeps wp_options small. wp_parse_args() is what makes adding a setting in a later version safe without writing a migration.
/**
* Read settings with defaults merged in, so a key added in a later
* release does not produce an undefined-index notice on old installs.
*/
function acme_settings(): array {
static $cache = null; // get_option() is cached, but this avoids the merge.
if ( null !== $cache ) {
return $cache;
}
$defaults = array(
'api_key' => '',
'interval' => 300,
'log_level' => 'error',
);
$cache = wp_parse_args( get_option( 'acme_settings', array() ), $defaults );
return $cache;
}
function acme_update_setting( string $key, $value ): bool {
$settings = acme_settings();
$settings[ $key ] = $value;
// One row rewritten, not one row per setting.
return update_option( 'acme_settings', $settings, false );
}
Discussion