Options and transients

Store settings with the Options API, cache expensive results with transients, and keep autoloaded data small.

  • Intermediate
  • 9 min read
  • Updated 17 Oct 2026

What you will learn

  • Store options correctly
  • Control autoloading for each option
  • Register settings with a sanitize callback
  • Cache with transients
  • Invalidate cached data when the source changes

Before you start

Every plugin needs somewhere to keep its settings, and most need somewhere to keep the result of slow work, such as a call to an external API. WordPress provides both: the Options API for values that should last, and transients for values that can expire. Both are simple to use and easy to misuse, so this lesson covers the habits that keep a site fast.

The Options API

Options are named values stored in the wp_options table (with your site's prefix). The four functions you need:

PHP
<?php

// Read, with a default if the option does not exist.
$per_page = (int) get_option( 'myplugin_per_page', 10 );

// Create or change.
update_option( 'myplugin_per_page', 12 );

// Create only if it does not exist yet.
add_option( 'myplugin_installed_at', time() );

// Remove.
delete_option( 'myplugin_per_page' );

update_option() creates the option if it is missing, so you rarely need add_option(). It returns false both on failure and when the new value equals the old one, so do not treat false as an error on its own.

Store arrays as one option

Options can hold strings, numbers, booleans and arrays. WordPress serialises arrays and objects automatically. Group related settings into one option rather than creating many:

PHP
<?php

$defaults = array(
    'per_page'    => 10,
    'show_prices' => true,
    'layout'      => 'grid',
);

$settings = wp_parse_args( get_option( 'myplugin_settings', array() ), $defaults );

if ( $settings['show_prices'] ) {
    // ...
}

One option means one row, one cache entry and one place to clean up on uninstall. wp_parse_args() fills in missing keys, which keeps code working after you add a new setting.

Prefix every option name, like everything else. An option called settings or api_key will collide with another plugin sooner or later.

Autoloading

Some options are autoloaded: WordPress loads all of them in a single query early in every request and keeps them in memory. That is why get_option( 'blogname' ) costs nothing.

The trade-off is that every autoloaded option is loaded on every page, whether it is used or not. A plugin that autoloads a 2 MB array slows down the whole site, including pages that never use it.

Control it with the third argument of update_option() (and the fourth of add_option()):

PHP
<?php

// Needed on most front-end requests: autoload.
update_option( 'myplugin_settings', $settings, true );

// Only used on one admin screen or in a cron job: do not autoload.
update_option( 'myplugin_import_log', $log, false );

Current versions of WordPress decide automatically when you pass nothing, and avoid autoloading very large values by default, but being explicit makes the intent clear. Autoload small settings used on most requests. Do not autoload logs, caches, large arrays or anything used only in the admin.

To see how much a site autoloads in total, and the size of each autoloaded option, use WP-CLI:

Shell
wp option list --autoload=on --format=total_bytes
wp option list --autoload=on --fields=option_name,size_bytes

Recent versions of Site Health also warn when autoloaded options grow large. The performance lesson later in the course shows how to trim them.

The Settings API

For settings pages, register_setting() connects an option to a sanitise callback, so WordPress cleans the value every time it is saved, from any form:

PHP
<?php

add_action( 'admin_init', 'myplugin_register_settings' );

function myplugin_register_settings(): void {
    register_setting(
        'myplugin',
        'myplugin_settings',
        array(
            'type'              => 'array',
            'sanitize_callback' => 'myplugin_sanitize_settings',
            'default'           => array(),
        )
    );
}

function myplugin_sanitize_settings( $input ): array {
    $input  = is_array( $input ) ? $input : array();
    $layout = sanitize_key( $input['layout'] ?? 'grid' );

    return array(
        'per_page'    => min( 50, max( 1, absint( $input['per_page'] ?? 10 ) ) ),
        'show_prices' => ! empty( $input['show_prices'] ),
        'layout'      => in_array( $layout, array( 'grid', 'list' ), true ) ? $layout : 'grid',
    );
}

A form that posts to options.php and calls settings_fields( 'myplugin' ) then gets the nonce check and capability check (manage_options by default) for free. The sanitising lesson's rules still apply inside the callback: allowlists, ranges and the right sanitise function for each field.

Transients

A transient is a cached value with an expiry time. Use it for results that are slow to produce and fine to be slightly out of date.

PHP
<?php

set_transient( 'myplugin_rates', $rates, HOUR_IN_SECONDS );

$rates = get_transient( 'myplugin_rates' ); // false if missing or expired

delete_transient( 'myplugin_rates' );

WordPress defines time constants for readability: MINUTE_IN_SECONDS, HOUR_IN_SECONDS, DAY_IN_SECONDS, WEEK_IN_SECONDS, MONTH_IN_SECONDS and YEAR_IN_SECONDS.

Caching an external API call

The classic use: avoid calling a remote service on every page load.

PHP
<?php

/**
 * Upcoming public holidays from a remote API, cached for twelve hours.
 *
 * @return array List of holidays, or an empty array if unavailable.
 */
function myplugin_get_holidays(): array {
    $holidays = get_transient( 'myplugin_holidays' );

    if ( false !== $holidays ) {
        return $holidays;
    }

    $response = wp_remote_get(
        'https://api.example.com/holidays?country=GB',
        array( 'timeout' => 5 )
    );

    if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
        // Cache the failure briefly so a down API does not slow every request.
        set_transient( 'myplugin_holidays', array(), 5 * MINUTE_IN_SECONDS );
        return array();
    }

    $data     = json_decode( wp_remote_retrieve_body( $response ), true );
    $holidays = is_array( $data ) ? $data : array();

    set_transient( 'myplugin_holidays', $holidays, 12 * HOUR_IN_SECONDS );

    return $holidays;
}

Points to notice:

  • get_transient() returns false when there is nothing cached, so never store false itself. Store an empty array or 0 instead, as the failure branch does.
  • The request has a short timeout. Without it, a slow remote server makes your page slow.
  • A failure is cached for a few minutes, so a broken API does not cause a remote call on every request.
  • Data from the API is untrusted input. Escape it when you print it, like any other data.

Where transients live

Without a persistent object cache, transients are stored in the options table as two rows: the value and its expiry time. They are not autoloaded when they have an expiry. Expired transients are deleted when read, and in a regular clean-up.

With a persistent object cache such as Redis or Memcached, transients are stored in that cache instead and never touch the database. The object cache lesson covers this.

A transient can disappear at any time, before its expiry: a cache flush, memory pressure or a server restart can remove it. Treat it as a cache, never as storage. If losing the value would lose data, use an option or a custom table.

Invalidation

An expiry time is the simplest way to refresh a cache. When your own data changes, delete the transient straight away so visitors see the update:

PHP
<?php

function myplugin_get_upcoming_count(): int {
    $count = get_transient( 'myplugin_upcoming_count' );

    if ( false === $count ) {
        $query = new WP_Query(
            array(
                'post_type'      => 'myplugin_event',
                'post_status'    => 'publish',
                'posts_per_page' => 1,
                'fields'         => 'ids',
                'meta_query'     => array(
                    array(
                        'key'     => 'myplugin_event_date',
                        'value'   => wp_date( 'Y-m-d' ),
                        'compare' => '>=',
                        'type'    => 'DATE',
                    ),
                ),
            )
        );

        $count = (int) $query->found_posts;
        set_transient( 'myplugin_upcoming_count', $count, DAY_IN_SECONDS );
    }

    return (int) $count;
}

add_action( 'save_post_myplugin_event', 'myplugin_clear_event_caches' );
add_action( 'deleted_post', 'myplugin_clear_event_caches' );

function myplugin_clear_event_caches(): void {
    delete_transient( 'myplugin_upcoming_count' );
}

save_post_{post_type} fires whenever an event is saved, so the count is rebuilt on the next request. A count of 0 is stored as 0, not false, so it is still cached.

Naming and limits

  • Prefix transient names, and keep them to 172 characters or fewer.
  • If a name includes variable parts, such as a user ID or query arguments, build it predictably, for example 'myplugin_events_' . md5( wp_json_encode( $args ) ).
  • On Multisite, set_site_transient() and get_site_transient() share a value across the whole network.

Choosing where to store data

DataStore in
Plugin settings, small and used oftenAn autoloaded option, as one array
Large or rarely used valuesAn option with autoload off
Slow results that can be rebuiltA transient, or the object cache
Data about a post, user or termMeta
Many structured records you queryA custom table

Summary

  • Use get_option() with a default, update_option() and delete_option(); prefix names and group settings into one array.
  • Autoload only small values needed on most requests; turn autoload off for anything large or admin-only.
  • register_setting() with a sanitize_callback cleans settings on every save.
  • Transients cache values with an expiry; false means "not cached", so never store false.
  • Transients can vanish at any time and move to the object cache when one is installed; delete them when the underlying data changes.

Further reading