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
// 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
$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
// 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:
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
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
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
/**
* 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()returnsfalsewhen there is nothing cached, so never storefalseitself. Store an empty array or0instead, 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
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()andget_site_transient()share a value across the whole network.
Choosing where to store data
| Data | Store in |
|---|---|
| Plugin settings, small and used often | An autoloaded option, as one array |
| Large or rarely used values | An option with autoload off |
| Slow results that can be rebuilt | A transient, or the object cache |
| Data about a post, user or term | Meta |
| Many structured records you query | A custom table |
Summary
- Use
get_option()with a default,update_option()anddelete_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 asanitize_callbackcleans settings on every save.- Transients cache values with an expiry;
falsemeans "not cached", so never storefalse. - Transients can vanish at any time and move to the object cache when one is installed; delete them when the underlying data changes.