The Options API

Reading and writing options, what autoload costs you, and the serialization WordPress does on your behalf.

Syntaxget_option( string $option, mixed $default_value = false ): mixed

Options 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

Example · php
// 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.

Example · php
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.

Example · php
// 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.

Example · php
/**
 * 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

  • Be the first to comment on this lesson.