Once you start building more complex things, you may find that you need to implement some kind of caching of your own. In this article I’ll explain how to do so with transients and how this compares to using the options or wp_cache_* methods directly.
WordPress has a number of caching mechanisms built into it to improve performance without you having to do anything. WP_Query’s results are cached internally in various ways, the data fetching done by blocks is cached to improve performance, and so on.
When you can cache the result of a database query or API request, you store the result of that request in memory (i.e. object cache) or in the database as an option. So when that value is requested again, you can get that previous value without having to do the query or make the request itself.
If it’s a super small amount of data, it probably won’t be as beneficial. But if it’s a larger dataset, then this might help speed up page performance in a big way.
Using the Transients API vs Object Cache API
The Transients API and the WP Object Cache both have methods you can call to cache values. So which one should you choose? The answer is simple but not exactly obvious.
The Transients API implements the Object Cache and Options API under the hood based on how your install is set up on the server. If your server has a persistent object cache set up like Redis or memcached, then the Transients API will store the values in the object cache. Otherwise, the values are cached to the options table.
If you call wp_cache_set directly without a persistent object cache setup, the result is only cached in memory for the current request. This means each new person that hits the page is still having to request the data again.
The whole point of object caching is to not have to make the same query or request on every page load. So if you’re using wp_cache_set directly you may be missing out on the fallback behavior from the Transients API.
While the Transients API does store cached values in the options table as a fallback, options are typically not the best thing to use directly for caching either.
That said, in 2026 and beyond I would expect any host worth its salt to provide its customers with a persistent object cache, in which case you’ll be leveraging that instead of the options table fallback anyway.
One key thing to remember about transients: their expiration time is a maximum time, not a minimum. A stored transient value could be cleared at any time between its creation and the expiry time.
Options are state, not cache
As noted above, options can be used to store cached data but they’re not the best choice to use for data that can potentially become stale.
Options are autoloaded by default and often devs forget to disable autoloading. This isn’t always a problem, but if your site is autoloading one or more big options this can cause the entire site’s performance to suffer.
Options are best suited to store data based on something a user has done (ex: Admin settings, field updates, etc). You should not reach for these to store the result of an API request or database query.
Building the radar
Before getting into code, here’s the rough decision tree I run through when I’m looking at a piece of code and asking “should this be cached?”:
- Is this expensive? (Network call, slow query, complex computation.) If no, don’t cache. The lookup itself has a cost.
- Is the result reused? Either within a single request, across requests, or both. If it’s only computed once and never accessed again, no point caching.
- Can the result tolerate being slightly stale? Most things can. Some can’t (account balances, real-time inventory, anything with strict consistency requirements).
- Do I know what server/hosting this code will run on? If the site doesn’t have an object cache or minimal server resources, that may influence my decision to include the query in question at all.
Knowing when (and when not) to cache something is very important. Over-caching values that don’t need it provides no benefit at best, and at worst it causes performance issues of its own.
Now, let’s discuss a few examples of when you might actually want to cache some data.
Example 1: caching a custom REST endpoint
Picture an endpoint that returns the latest releases from a third-party API (GitHub here, but the pattern is the same for any external service). The endpoint hits the network, parses JSON, and returns the result.
Without caching, every hit to your endpoint is a hit to GitHub’s API. That’s slow, it can fail, and you’ll burn through rate limits if anyone’s actually using the endpoint.
<?php
add_action('rest_api_init', function () {
register_rest_route('mytheme/v1', '/releases', [
'methods' => 'GET',
'callback' => 'mytheme_get_releases',
'permission_callback' => '__return_true',
]);
});
function mytheme_get_releases() {
$cache_key = 'mytheme_github_releases';
$releases = get_transient( $cache_key );
if ( false === $releases ) {
$response = wp_remote_get(
'https://api.github.com/repos/WordPress/wordpress-develop/releases'
);
if ( is_wp_error($response) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error(
'fetch_failed',
'Could not fetch releases',
[ 'status' => 502 ]
);
}
$releases = json_decode( wp_remote_retrieve_body( $response ), true );
set_transient( $cache_key, $releases, HOUR_IN_SECONDS );
}
return rest_ensure_response( $releases );
}
A few things worth pointing out:
- When you’re checking the value from
get_transient, use a strict comparison to determine if it’s false so something like a0won’t evaluate incorrectly1. - The expiration matches how often the underlying data realistically changes. WordPress releases don’t drop hourly, so you could go longer (a day, even), but an hour is a sensible default for most cases.
You could go further from here (caching an error state briefly so a flaky API doesn’t get hammered, returning stale data on failure rather than an error response), but those are optimizations on top of the same pattern.
Example 2: caching an expensive popular posts query
Now the other side of the coin. A query that hits your own database, sorts by a meta key, and shows up in a sidebar widget on every page of the site.
The query itself isn’t complicated, but meta_value_num sorting is doing real work, especially as the post count grows. And because the widget appears on every page, that work happens on every uncached page load.
<?php
function mytheme_get_popular_post_ids() {
$cache_key = 'popular_posts';
$post_ids = get_transient( $cache_key );
if (false === $post_ids) {
$query = new WP_Query( [
'posts_per_page' => 5,
'meta_key' => 'post_views',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'no_found_rows' => true,
'fields' => 'ids',
] );
$post_ids = $query->posts;
set_transient( $cache_key, $post_ids, DAY_IN_SECONDS );
}
return $post_ids;
}
A few things to call out:
'fields' => 'ids'keeps the cached payload small. You can hydrate post objects later withget_post(), and core already caches those individually, so you’re not paying twice.'no_found_rows' => trueskips theSQL_CALC_FOUND_ROWSquery you don’t need for a fixed-size widget.
The above args make the query faster when it actually runs and we store the result in a transient with a maximum age of one day. For something like popular posts, having that update once a day is probably fine.
Remember to consider the appropriate expiration time for your transient when you go to write your own and don’t forget it’s a maximum not a minimum time.
When to reach for wp_cache_set instead
If you need to have direct control over the values of a given cache group or set all of your cached values to a specific group, then you may want to use the Object Cache directly instead of the Transients API.
As noted above, you cannot set the group of a transient. When you call set_transient, if you’re using a persistent object cache, wp_cache_set is called with a cache group of transient. This is not something you can filter or provide a different value for.
So if you needed to set the cache group yourself and you knew that the user had a persistent object cache in place, you could use wp_cache_set directly instead of set_transient. You can then choose the cache group and all other params for the cache item.
If you wanted to be able to selectively purge the cache of only certain items, you’ll need a cache group to distinguish them. For most builds you’ll be doing for clients, you’re unlikely to ever need to do this. Most of the time, the Transients API is just what you need.
Further reading
- Transients API
- WP_Object_Cache class reference
- wp_cache_get() function reference
- WordPress VIP’s caching documentation
- Redis (Wikipedia)
- Memcached (Wikipedia)
Happy coding!
- As such, you shouldn’t actually set boolean values in transients. If you do need to store a boolean, just convert it to an integer instead (0 or 1). ↩︎