Himanshu SharmaSoftware engineer
Let’s talk

WordPress · Lesson 36 of 45

Caching with transients

Cache slow results such as API calls and heavy queries with transients, set expiry, clear them when content changes, and know the object cache.

12 minData and APIsUpdated 23 October 2026

By the end of this lessonStore a slow result temporarily with set_transient() and get_transient(), refresh it correctly, and know when a transient is the wrong tool.

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
<?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' ) );
result
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
<?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
<?php
[ $count, $source ] = bakery_slow_count();
echo "$count ($source)\n";
[ $count, $source ] = bakery_slow_count();
echo "$count ($source)\n";
result
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:

php
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
<?php
add_action( 'save_post', function () {
	delete_transient( 'bakery_post_count' );
} );
php
<?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";
result
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_options table 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() and wp_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:

bash
wp transient set bakery_demo "hello" 600
wp transient get bakery_demo
wp option get _transient_bakery_demo
wp transient delete bakery_demo
result
Success: 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 0 never 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 valid 0 or 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
php
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.

Check yourself

1. What does get_transient() return when the transient has expired?
2. What is a good use of a transient?
3. Where are transients stored by default?
4. When should you delete a transient early?