Skip to content
Performance Optimization

WordPress Transients API: Cache Database Queries for Better Performance

· · 7 min read
Banner for WordPress Transients API caching deep dive

The Transients API is the right answer to about 70% of the “this WordPress query is slow” problems I get paged on, and the wrong answer to the other 30%. Knowing the difference is what separates a site that scales quietly from a site where wp_options has ballooned to 340MB of orphaned transients and every admin page takes eight seconds to load.

I have spent more time than I would like untangling transient messes on client installs, and the patterns are always the same: a plugin developer cached something with good intentions, the cleanup was never wired up, the site grew, and five years later it is costing the client $80 per month in extra hosting just to tolerate the bloat. This piece is the deep dive I wish every WordPress developer read before their first set_transient call.

The core API, the three functions you actually use

There are three primary functions, plus their multisite equivalents. Most plugins only ever touch these six.

And their network-wide equivalents for multisite installs:

WordPress ships a set of readable time constants: MINUTE_IN_SECONDS, HOUR_IN_SECONDS, DAY_IN_SECONDS, WEEK_IN_SECONDS, MONTH_IN_SECONDS, and YEAR_IN_SECONDS. Always use these instead of raw numbers. Your future self, reviewing the diff in six months, will thank you for writing 12 * HOUR_IN_SECONDS instead of 43200.

Where transients actually live

This is the single most misunderstood thing about the Transients API, and the root cause of almost every bloat problem I have diagnosed. Transients are stored in one of two places, and which place matters enormously.

If an external object cache is installed, like Redis or Memcached via a drop-in such as Redis Object Cache, transients go there. They never touch the database. Expiration is honored by the cache backend and is reliable. This is the happy path and it is what every production WordPress site should be on.

If no object cache is installed, transients go into wp_options as two rows per transient, one for the value (_transient_KEY) and one for the timeout (_transient_timeout_KEY). Expiration is lazy. The row is only deleted when someone calls get_transient() for it and finds it expired.

Have you caught the trap yet? If you set a transient that nothing ever reads back, like a cached error response, a background job status, or a cache for an admin-only page that rarely gets hit, those rows accumulate forever. WordPress 6.3 added automatic cleanup of expired transients during wp_scheduled_delete, but plenty of older installs still carry orphans from before that fix landed. I run an options table audit on every client handoff as part of my WordPress database optimization routine, and it is usually good for trimming 20% to 60% of the table size.

The expensive query pattern

This is the canonical reason to reach for transients, caching the output of a query that is expensive to compute and read from many places.

Three things to get right every time:

  1. Check for false specifically. get_transient() returns false on a miss or expiry, and your cached data otherwise. If the data you cached could legitimately be false, for example the result of a failed API call, wrap it in an array like [ 'data' => $value ] so you can distinguish “no cache” from “cached a false value.”
  2. Version the key. When the shape of the cached data changes, bump the version suffix, for example my_popular_posts_v1 becomes my_popular_posts_v2. Otherwise old cached shapes collide with new code after a deploy and you spend an afternoon debugging an “undefined index” notice that should never have happened.
  3. Invalidate on writes. If the underlying data can change, a new post, a new comment, a meta update, clear the transient in the appropriate action hook. Caches you never invalidate become the thing your users complain about at 3 AM.

Remote API caching with negative-cache discipline

The other canonical use case is caching an external API response so you are not hammering a third party on every pageview. Here is the pattern I use on every client install that pulls weather, exchange rates, inventory, or anything else over the network.

Notice the brief negative cache on failure. Without that fallback, a broken third-party API turns into an accidental denial-of-service on your own site, because every request retries the same failing call, and your PHP workers queue up waiting on a socket that is never going to answer. I learned this the hard way during a 2024 outage where an exchange rate API went down, our client’s store had no negative cache, and we fell over because every single pageview was waiting 30 seconds on a dead connection.

Autoload, the silent killer

Every row in wp_options has an autoload flag. Rows with autoload = 'yes' are loaded on every single page request as part of the alloptions bootstrap. This one query runs before anything else, and its size directly affects the latency of every request on the site.

Transients created with set_transient() get autoload = 'no' by default, which is correct. Transients created with set_site_transient() on a non-multisite install also avoid autoload.

But here is the trap. If you manually write to options with add_option or update_option to back a custom cache of your own, the default is autoload = 'yes'. A handful of those, each a few hundred kilobytes, and you have silently added a megabyte to the query that runs on every pageview. Always pass 'no' as the third argument to add_option or update_option when the value is cache-like. I have seen this kill sites that were otherwise perfectly tuned, and the diagnosis is one query away:

SELECT option_name, LENGTH(option_value) FROM wp_options WHERE autoload = 'yes' ORDER BY LENGTH(option_value) DESC LIMIT 20;

Run that on any suspect install. If the top row is more than 100KB, you have found the smoking gun.

When transients are the wrong tool

Three cases where I steer developers away from transients entirely:

  1. Hot data read thousands of times per request. The transient itself is still a wp_options query in the absence of an object cache. If you need true in-memory caching within a single request, use a PHP static variable, or wp_cache_* with a short-lived key.
  2. Data that must be exactly fresh. Transient expiration is approximate, not exact. For something like currency conversion on a live checkout, calculate fresh every time, or accept a short TTL you explicitly set to the tolerance you can live with. Do not hide the staleness behind a cache.
  3. High-volume, short-TTL caches. Writing thousands of transients per minute to wp_options on a site with no object cache is a write storm. Your replication falls behind. Your backups explode. This is the case where installing Redis stops being optional.

The Object Cache API alternative

When Redis or Memcached is installed, wp_cache_set and wp_cache_get are strictly more powerful than transients. I reach for them whenever I am writing a plugin for a controlled deployment where I know object cache is available.

The differences are worth knowing:

  • No database fallback. If there is no object cache, wp_cache_* is per-request only, meaning the cache is thrown away at the end of the request.
  • Cache groups let you invalidate swaths of keys at once, which is impossible with transients.
  • No wp_options bloat, ever, under any circumstances.

Use transients when you want your code to still work on sites with no object cache, the default case for most public plugins. Use wp_cache_* when you control the deployment and know object cache is installed. If your plugin is running on WP Engine, Kinsta, Pressable, or any managed host with object cache baked in, skip transients entirely and write against wp_cache_* directly.

Debugging transients in practice

The Query Monitor plugin shows every get_transient and set_transient call per request, and whether they hit or missed. I use it to catch two failure modes that otherwise stay invisible. The first is a transient that is set but never read, a dead cache that is just costing you writes. The second is a transient that is read on every request but never set, a broken write path where the cache is permanently missing and the underlying query runs every time anyway.

For production diagnostics, WP-CLI is your friend:

If you are running scheduled cleanup as part of your maintenance workflow, pair the cleanup with the guidance in my WP-Cron deep dive, because a cleanup task that never fires because WP-Cron is broken is a cleanup task that does nothing.

The bottom line

Transients are the right caching tool for expensive database queries that are read from multiple pages, remote API responses that tolerate a few minutes of staleness, and any computation whose inputs change predictably. They are the wrong tool for per-request memoization, hard-freshness data, and high-write-volume caches on a site without an object cache.

On any production WordPress site, my single strongest recommendation is to install Redis Object Cache and stop caring about which internal storage transients use. Object cache makes the question moot and removes an entire class of performance bug from your life. The $5 per month Redis adds to your hosting bill is the cheapest performance optimization money can buy in WordPress, and I have never once regretted recommending it to a client.