Why cache?
Some work is slow: a request to another website, a heavy database query, a big calculation. If every visitor triggered it, the site would be slow and might hit the other service's limits. A cache keeps the result for a while and reuses it. WordPress has the Transients API for exactly this: a cached value with an expiry time.
The three functions
<?php
// Nothing cached yet: false.
var_dump( get_transient( 'bakery_popular' ) );
// Store a value for 10 minutes (600 seconds).
set_transient( 'bakery_popular', [ 'Sourdough', 'Rye', 'Focaccia' ], 10 * MINUTE_IN_SECONDS );
print_r( get_transient( 'bakery_popular' ) );
delete_transient( 'bakery_popular' );
var_dump( get_transient( 'bakery_popular' ) );bool(false)
Array
(
[0] => Sourdough
[1] => Rye
[2] => Focaccia
)
bool(false)MINUTE_IN_SECONDS, HOUR_IN_SECONDS, DAY_IN_SECONDS and WEEK_IN_SECONDS are constants that make times readable. Arrays and objects are stored for you.
The standard pattern
Always write transient code in the same shape: try the cache; if it is empty, do the slow work, store it, and return it. Here it is in a small must-use plugin, so the examples that follow can use the function:
<?php
function bakery_slow_count(): array {
$cached = get_transient( 'bakery_post_count' );
if ( false !== $cached ) {
return [ (int) $cached, 'from cache' ];
}
// Imagine a heavy query or a remote request here.
$count = (int) wp_count_posts()->publish;
set_transient( 'bakery_post_count', $count, HOUR_IN_SECONDS );
return [ $count, 'computed' ];
}<?php
[ $count, $source ] = bakery_slow_count();
echo "$count ($source)\n";
[ $count, $source ] = bakery_slow_count();
echo "$count ($source)\n";1 (computed)
1 (from cache)The first call does the work; the second reads the stored value. Compare to false with !==, because 0 or an empty array are valid cached values.
Caching a remote request
Remote requests are the most common reason to use transients. Check for errors, and cache failures briefly so a broken service is not hit on every page view:
function bakery_get_weather(): ?array {
$cached = get_transient( 'bakery_weather' );
if ( false !== $cached ) {
return $cached ?: null; // '' means a cached failure
}
$response = wp_remote_get( 'https://api.example.com/weather?city=London', [ 'timeout' => 5 ] );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
set_transient( 'bakery_weather', '', 5 * MINUTE_IN_SECONDS ); // try again in 5 minutes
return null;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
set_transient( 'bakery_weather', $data, HOUR_IN_SECONDS );
return $data;
}This example is not run because it needs a live service. wp_remote_get() is WordPress's HTTP function: it works on any host, handles redirects and TLS, and returns a WP_Error on failure.
Keeping the cache fresh
Waiting for the expiry can show stale data. Delete the transient when the underlying data changes, using a hook:
<?php
add_action( 'save_post', function () {
delete_transient( 'bakery_post_count' );
} );<?php
[ $count, $source ] = bakery_slow_count();
echo "before: $count ($source)\n";
[ $count, $source ] = bakery_slow_count();
echo "again: $count ($source)\n";
wp_insert_post( [ 'post_title' => 'New bread', 'post_status' => 'publish' ] ); // fires save_post
[ $count, $source ] = bakery_slow_count();
echo "after: $count ($source)\n";before: 1 (from cache)
again: 1 (from cache)
after: 2 (computed)Saving a post cleared the cache, so the count was computed again and includes the new post.
Transients, options and the object cache
- A transient is a cache, and caches can disappear at any time: memory pressure, a cache flush, a plugin clearing everything. Your code must always be able to rebuild the value. Never keep important data (user input, settings) in a transient; use an option.
- By default, transients are saved in the
wp_optionstable with a second row for the expiry time. Expired rows are deleted when they are read or by a cleanup task. - On sites with a persistent object cache (Redis or Memcached through a plugin), transients are stored in that fast memory instead and never touch the database. Your code does not change.
- WordPress also has an in-request object cache:
wp_cache_set()andwp_cache_get()keep values only until the end of the request (or longer with a persistent cache). Use it to avoid repeating a query within one page view.
WP-CLI can work with transients:
wp transient set bakery_demo "hello" 600
wp transient get bakery_demo
wp option get _transient_bakery_demo
wp transient delete bakery_demoSuccess: Transient added.
hello
hello
Success: Transient deleted.The wp option get line shows the default storage: the value sits in wp_options under the name _transient_bakery_demo, next to a _transient_timeout_ row.
Naming and limits
- Prefix names with your plugin (
bakery_weather). In the database the full option name is_transient_bakery_weather; names are limited to 172 characters. - A transient with expiry
0never expires, but it is still just a cache. - For different values per user, language or request, put that in the key:
'bakery_menu_' . get_current_user_id(). - Do not cache per-visitor, uncacheable data (like a cart) unless the key makes it unique.
Common mistakes
- Testing with
if ( $cached ), so a valid0or empty list triggers the slow work every time. - Caching forever and never clearing it.
- Using a transient for data you cannot recreate.
- Making the key depend on user input without limit, which fills the table with entries.
- Caching an error as if it were a result.
Exercise
Write bakery_latest_titles() that returns the titles of the 3 newest posts and caches them for 15 minutes in a transient named bakery_latest_titles.
Show one possible answer
function bakery_latest_titles(): array {
$titles = get_transient( 'bakery_latest_titles' );
if ( false === $titles ) {
$posts = get_posts( [ 'numberposts' => 3, 'orderby' => 'date', 'order' => 'DESC' ] );
$titles = wp_list_pluck( $posts, 'post_title' );
set_transient( 'bakery_latest_titles', $titles, 15 * MINUTE_IN_SECONDS );
}
return $titles;
}To keep it fresh, also call delete_transient( 'bakery_latest_titles' ) on the save_post hook.
Key points
- A transient is a cached value with an expiry time: set_transient( 'name', $value, $seconds ) and get_transient( 'name' ).
- get_transient() returns false when the value is missing or expired, so recompute and store it again.
- Delete a transient with delete_transient() when the data it caches changes, instead of waiting for the expiry.
- Without a persistent object cache, transients live in wp_options; with Redis or Memcached they live in memory. Never rely on a transient still existing.