
How to Use the WordPress Transients API to Cache Data?
Some work is too slow to repeat on every page load. Fetching follower counts from a third-party API, running a complex WP_Query with meta and taxonomy conditions, or building a "most popular posts" list from thousands of rows all add hundreds of milliseconds, sometimes whole seconds, to every request that triggers them. If the result only changes every few minutes or hours, recalculating it for every visitor is wasted effort.
The Transients API is WordPress's built-in answer to this. It lets you store the result of expensive work under a name, with an expiration time, and read it back cheaply on later requests. It takes three functions to learn, works on every WordPress install without extra setup, and automatically upgrades to an in-memory cache like Redis or Memcached when one is available.
This guide covers how transients work, how to use them for real caching problems, how to clear them at the right time, and the mistakes that cause stale data or slow sites.
What Is a Transient?
A transient is a piece of cached data with three properties:
- A name (the key you use to read and write it).
- A value (any PHP data that can be serialized: a string, number, array, or object).
- An expiration in seconds, after which WordPress treats it as gone.
The important detail is where WordPress stores it:
- By default, transients are saved in the
wp_optionsdatabase table as two rows:_transient_{name}holds the value and_transient_timeout_{name}holds the expiration timestamp. - When a persistent object cache is installed (such as Redis or Memcached via a drop-in), transients skip the database entirely and are stored in that cache instead.
Your code is the same either way. That's the main reason to use transients rather than writing your own caching table or abusing update_option(). If you later set up object caching with Redis in WordPress, every transient on the site gets faster automatically.
The Three Core Functions
The whole API revolves around three functions:
// Store a value. Returns true on success, false on failure.
set_transient( string $transient, mixed $value, int $expiration = 0 );
// Read a value. Returns the value, or false if it doesn't exist or has expired.
get_transient( string $transient );
// Remove a value. Returns true if it was deleted, false otherwise.
delete_transient( string $transient );
A few rules about the arguments:
- The name must be 172 characters or fewer. WordPress prefixes it with
_transient_timeout_when storing it in the database, and theoption_namecolumn is limited to 191 characters. - The value is serialized automatically. Pass arrays and objects directly. Don't call
serialize()orjson_encode()yourself. - The expiration is in seconds. WordPress provides readable constants:
MINUTE_IN_SECONDS,HOUR_IN_SECONDS,DAY_IN_SECONDS,WEEK_IN_SECONDS,MONTH_IN_SECONDS, andYEAR_IN_SECONDS.
The Basic Caching Pattern
Almost every use of transients follows the same three steps: try the cache, and if it's empty, do the work and save the result.
function tw_get_expensive_data() {
$data = get_transient( 'tw_expensive_data' );
if ( false === $data ) {
// Cache miss: do the slow work.
$data = tw_calculate_expensive_data();
// Save it for an hour.
set_transient( 'tw_expensive_data', $data, HOUR_IN_SECONDS );
}
return $data;
}
The first visitor after the transient expires pays the cost of the slow work. Everyone else for the next hour gets the cached result.
Note the strict comparison: false === $data. get_transient() returns false to mean "not found," so a loose check like if ( ! $data ) would treat a legitimately cached empty array, 0, or empty string as a cache miss and recalculate it every time.
Example 1: Caching a Remote API Request
External HTTP requests are the classic use case. They're slow, they can fail, and many APIs rate-limit you. Here's a function that fetches a GitHub repository's star count and caches it for six hours:
function tw_get_github_stars( $repo ) {
$cache_key = 'tw_gh_stars_' . md5( $repo );
$stars = get_transient( $cache_key );
if ( false !== $stars ) {
return (int) $stars;
}
$response = wp_remote_get(
'https://api.github.com/repos/' . $repo,
[
'timeout' => 5,
'headers' => [ 'Accept' => 'application/vnd.github+json' ],
]
);
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
// Cache the failure briefly so a broken API doesn't slow down every request.
set_transient( $cache_key, 0, 5 * MINUTE_IN_SECONDS );
return 0;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$stars = isset( $body['stargazers_count'] ) ? (int) $body['stargazers_count'] : 0;
set_transient( $cache_key, $stars, 6 * HOUR_IN_SECONDS );
return $stars;
}
Use it anywhere in a template:
<p>
<?php
printf(
esc_html__( '%s stars on GitHub', 'tidewave' ),
number_format_i18n( tw_get_github_stars( 'WordPress/wordpress-develop' ) )
);
?>
</p>
Three details make this production-ready:
- The cache key includes the input. Hashing
$repowithmd5()gives each repository its own transient and keeps the name short and safe no matter what's passed in. - Failures are cached too, but for a shorter time. Without this, an API outage means every page load waits for a five-second timeout. Caching the failure for five minutes protects your site's response time while still retrying soon.
- The cached value is never
false. On failure it stores0, whichget_transient()can tell apart from a cache miss.
Example 2: Caching an Expensive Database Query
A "popular posts" widget that sorts by a custom view-count meta field runs a meta query across every published post. That's fine on a small blog and slow on a large one. Cache the post IDs:
function tw_get_popular_post_ids( $count = 5 ) {
$cache_key = 'tw_popular_posts_' . absint( $count );
$post_ids = get_transient( $cache_key );
if ( false === $post_ids ) {
$query = new WP_Query(
[
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $count,
'meta_key' => 'tw_view_count',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'fields' => 'ids',
'no_found_rows' => true,
'update_post_term_cache' => false,
]
);
$post_ids = $query->posts;
set_transient( $cache_key, $post_ids, 12 * HOUR_IN_SECONDS );
}
return $post_ids;
}
Then render them:
$popular_ids = tw_get_popular_post_ids( 5 );
if ( $popular_ids ) : ?>
<ul class="tw-popular-posts">
<?php foreach ( $popular_ids as $post_id ) : ?>
<li>
<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>">
<?php echo esc_html( get_the_title( $post_id ) ); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php endif;
Caching IDs rather than full WP_Post objects or rendered HTML is a deliberate choice. IDs are small to store, and get_permalink() and get_the_title() always return current data, so if someone edits a post title, the widget shows the new title right away even though the ranking is cached.
Clearing Transients When Data Changes
Expiration handles data that drifts slowly. But when you know the underlying data changed, don't wait for the timer. Delete the transient so the next request rebuilds it.
The cleanest way to do that is with a hook. For the popular posts example, clear the cache whenever a post is saved or deleted:
add_action( 'save_post_post', 'tw_clear_popular_posts_cache' );
add_action( 'deleted_post', 'tw_clear_popular_posts_cache' );
function tw_clear_popular_posts_cache() {
foreach ( [ 5, 10 ] as $count ) {
delete_transient( 'tw_popular_posts_' . $count );
}
}
If hooks are new to you, see how WordPress hooks, actions, and filters work.
Notice the loop: since the key includes $count, you need to delete each variation you use. This is the main cost of input-based cache keys. Keep the number of variations small and predictable, or use a version number in the key instead:
function tw_popular_cache_version() {
return (int) get_option( 'tw_popular_cache_version', 1 );
}
function tw_bump_popular_cache_version() {
update_option( 'tw_popular_cache_version', tw_popular_cache_version() + 1, false );
}
// Build keys like: tw_popular_posts_v3_5
$cache_key = 'tw_popular_posts_v' . tw_popular_cache_version() . '_' . absint( $count );
Bumping the version makes every old key unreachable in one step. The old entries expire and get cleaned up on their own.
Transients With No Expiration
If you omit the third argument or pass 0, the transient never expires:
set_transient( 'tw_site_stats', $stats ); // No expiration.
Be careful with this. When stored in the database, a transient without an expiration is autoloaded, meaning WordPress loads it into memory on every single request along with core options, whether or not anything uses it. A few small values are harmless. Large arrays saved this way quietly bloat every page load.
Transients with an expiration are not autoloaded, so they're only loaded from the database when you call get_transient(). In practice, always set an expiration, even a long one like WEEK_IN_SECONDS. If data truly needs to persist indefinitely, it's not a cache, and it belongs in a regular option or post meta.
Transients Are Not Guaranteed Storage
The expiration is a maximum lifetime, not a promise. A transient can disappear before it expires:
- An object cache like Redis or Memcached may evict it to free memory.
- A caching plugin or host may flush all transients.
- Someone may run a cleanup tool or
wp transient delete --all.
So your code must always be able to rebuild the value from scratch, which is exactly what the if ( false === $data ) pattern does. Never store anything in a transient that you can't regenerate, such as form submissions, user settings, or one-time tokens you'd lose track of. Those belong in options, user meta, or a custom table.
Site Transients for Multisite
On a WordPress multisite network, regular transients are stored per site. If the data is the same across the whole network, such as a license check or a remote feed shown on every site, use the network-wide versions:
set_site_transient( 'tw_license_status', $status, DAY_IN_SECONDS );
get_site_transient( 'tw_license_status' );
delete_site_transient( 'tw_license_status' );
On a single-site install, these behave exactly like the regular functions, so it's safe to use them in plugins that might run on either.
Managing Transients From the Command Line
WP-CLI has a full set of transient commands, which are handy for debugging and deployments:
# Read a single transient
wp transient get tw_expensive_data
# Delete one transient
wp transient delete tw_expensive_data
# Delete only expired transients
wp transient delete --expired
# Delete every transient (safe, since transients are only a cache)
wp transient delete --all
# Check whether transients are stored in the database or an object cache
wp transient type
WordPress also cleans up expired transients automatically through a daily scheduled event, so you don't need to purge them by hand on a healthy site. If your site's WordPress cron jobs aren't running reliably, expired rows can pile up in wp_options, and running wp transient delete --expired occasionally is a quick fix.
Common Mistakes With Transients
- Checking with
if ( ! $data )instead ofif ( false === $data ). Cached empty values look like misses and get recalculated every time. - Storing the boolean
falseas the value. It's impossible to tell apart from "not cached." Store0, an empty array, or a small marker string instead. - Leaving out the expiration. Large values get autoloaded on every request.
- Using one key for data that varies. If a result depends on the current user, language, or query arguments, include those in the key, or every visitor sees the first visitor's result.
- Caching per-user data on a high-traffic site. A transient per user per page can create millions of
wp_optionsrows. Use user meta or the object cache withwp_cache_set()for short-lived per-user data. - Never invalidating. Relying on expiration alone means editors see stale content after an update. Delete the transient on the hook that changes the data.
Frequently Asked Questions (FAQ) About the WordPress Transients API
Options are permanent settings that stay until you delete them. Transients are temporary cached data with an expiration time, and they can disappear at any moment. Use options for data you need to keep, like plugin settings, and transients for data you can always recalculate, like API responses or query results.
The object cache (wp_cache_set() and wp_cache_get()) only lasts for the current request unless a persistent cache like Redis is installed. Transients persist between requests on every site, because they fall back to the database when no persistent cache exists. Use transients when you need caching across requests that works everywhere.
In the wp_options table, as two rows per transient: _transient_{name} for the value and _transient_timeout_{name} for the expiration timestamp. Site transients use _site_transient_ prefixes and live in wp_sitemeta on multisite. With a persistent object cache, transients are stored there instead and don't touch the database.
Yes. WordPress removes expired transients when you try to read them, and it also runs a daily scheduled cleanup. If WP-Cron isn't running on your site, expired rows can build up, and wp transient delete --expired clears them out.
Yes. Transients are a cache by definition, so any well-written plugin or theme will rebuild them on the next request. Expect the first few page loads afterward to be slower while caches refill.
Match it to how stale the data can be before it's a problem. Stock prices or live scores might need a minute. Social follower counts can be cached for hours. Data that only changes when an editor updates something can use a long expiration plus delete_transient() on the relevant save hook.
They can if they're misused. Transients without an expiration are autoloaded on every request, and per-user or per-URL transients on a busy site can bloat the wp_options table. Always set an expiration, keep the number of unique keys bounded, and consider a persistent object cache on high-traffic sites.
Conclusion
The Transients API turns slow, repeated work into a fast lookup with three functions: get_transient(), set_transient(), and delete_transient(). Check the cache with a strict false === comparison, rebuild and store the value on a miss, always set an expiration, and delete the transient on the hook where the underlying data changes.
Start with the slowest parts of your site, which are usually remote API calls and complex queries, and wrap them in the basic caching pattern from this guide. Combined with a persistent object cache and the other techniques for speeding up a WordPress website, transients are one of the cheapest performance wins available in WordPress, and they don't need a single extra plugin.


