WordPress Transients: A Technical Guide to a Powerful API

Ryan Kanner (@CodeProKid)

slides.com/codeprokid/wcri17

Me.

  • WordPress Developer for 8+ years
  • EX east-coaster
  • Work for Digital First Media, working on websites for newspapers such as the Denver Post, Orange County Register, and Mercury News.

The Basics

What the heck are transients?

  • Transients provide a simple way to store data on your server and have it expire at defined intervals.
  • All transients have the following components
    • Key - A Unique identifier to find, update, or delete your transient.
    • Value - The data you want to cache. It could be a string, array, or object. Essentially anything you can store in an option.
    • Expiration - The maximum amount of time your data should be valid for. 

... sooooo, what are transients?

http://i.imgur.com/EAvvfw2.gifv

What are transients

  • Caching is a necessity in the modern web
  • We have browser caching for frontend assets, but what can we do about backend caching?
  • Transients can cache data for things like remote requests, complex queries, and data that doesn't change often
if ( false === ( $data = get_transient( 'my_transient' ) ) ) {

    $data = 'some stuff';

    set_transient( 'my_transient', $data, HOUR_IN_SECONDS );

}

echo $data;

How they work

  • They are essentially a key value store, with the added benefit of having a max-age.
  • When you go to retrieve the data, it does a check to see if it's passed it's max-age. If it has, it deletes the data, and will return false.
  • By default your site will use the database as it's storage engine, but if you are using an object cache (like memcache) it will skip the database, and only be stored in the cache.

Transient expiration times are a maximum time. There is no minimum age. Transients might disappear one second after you set them, or 24 hours, but they will never be around after the expiration time.

The API

Overview

  • The API consists of 3 functions...
    • set_transient
    • get_transient
    • delete_transient
  • Multisite has companion functions; set_site_transient(), get_site_transient(), and delete_site_transient().
  • When using an object cache, the functions rely on wp_cache_*.
  • Without an object cache the functions rely on *_option.
  • $transient(string) - Unique name to use when saving the transient. Becomes the "Key" in the key => value.
  • $value(mixed) - Data to be stored in the the transient. Can be an array, string, or an integer.
  • $expiration(int) - Optional integer passed to set max age. Adds _transient_timeout_$transient setting to options table, or uses built in object cache expiration.
  • @return(bool) - Returns true if successful, false if unsuccessful.
set_transient( $transient, $value, $expiration );
set_transient Hooks & Filters
  • pre_set_transient_{$transient} - exposes the value to be stored to the transient to a filter. Expiration and transient name are passed as context.
  • expiration_of_transient_{$transient} - exposes the expiration time to a filter. Transient name and value passed as context
  • set_transient_{$transient} - action that fires after a specific transient has been saved. Transient name, Value, and expiration passed as context.
  • setted_transient - action that fires after any transient is saved. Transient name, Value, and expiration passed as context.
  • $transient(string) - Name of the transient to retrieve. Will use get_option() to retrieve a transient saved in the database. Will use wp_cache_get() if saved in the object cache.
  • This function does a check to see if the transient data has expired. If it has, it will delete the data from the storage engine.
  • @return (bool|mixed) - If the transient is expired, or it doesn't exist, it will return false. Otherwise it will return the data stored in the transient.
get_transient( $transient );
get_transient Hooks & Filters
  • pre_transient_$transient - By default this value is set to false. This can be set to true via this filter to short circuit the process of returning a transients value. Transient name is passed as context.
  • transient_$transient - exposes value of the transient being returned to a filter. Transient name and value passed as context.
  • $transient(string) - Name of the transient to delete. Will use delete_option() to delete a transient saved in the database. Will use wp_cache_delete() if saved in the object cache.
  • This function will also delete the timeout (expiration) value from the database.
  • @return (bool) - Returns true if deletion is successful, false if it isn't.
delete_transient( $transient );
delete_transient Hooks & Filters
  • delete_transient_$transient - action that fires before a transient is actually deleted. Transient name is passed as context.
  • deleted_transient - action that fires after any transient is deleted. Transient name is passed as context.

Implementations

External API call


if ( false === ( $photos = get_transient( 'instagram_photos' ) ) ) {

    $url = add_query_arg( 
        [
            'access_token' => 'xxx-xxx-xxxx',
            'count' => 30,
        ],
        'https://api.instagram.com/v1/users/1234/media/recent/'
    );

    $response = wp_remote_get( esc_url( $url ), [ 'timeout' => 600 ] );

    $photos = $response['body'];

    set_transient( 'instagram_photos', $photos, 3 * HOUR_IN_SECONDS );

}

print_r( $photos );

WP Query

if ( false === ( $query = get_transient( 'query_transient' ) ) {
    $args = [
        'post_type' => 'post',
        'posts_per_page' => 100,
        'fields' => 'ids',
        'meta_query' => array(
            'AND',
            array(
                'key' => 'key_1',
                'value' => 'value_1',
            ),
            array(
                'key' => 'key_2',
                'value' => 'value_2',
            ),
        ),
    ];
    $post_ids = new WP_Query( $args );
    set_transient( 'query_transient', $post_ids, 0 );
}

echo '<ul>';
foreach ( $post_ids as $post_id ) {
    echo '<li>' . get_the_title( $post_id ) . '</li>';
}
echo '</ul>';

Dynamic Invalidation


function transient_flusher( $meta_id, $object_id, $meta_key ) {

    if ( 'key_1' === $meta_key || 'key_2' === $meta_key ) {
        delete_transient( 'query_transient' );
    }

}

add_action( 'updated_post_meta', 'transient_flusher', 10, 3 );

Shortfalls, quirks, and best practices

Transients can be kinda....

Shortfalls to the API

  • No soft expiration
  • Lot's of transients expiring at once. Workaround: MINUTE_IN_SECONDS + rand( 0, 60 )
  • No update locking
  • Cache stampeding
  • Managing many transients can get cumbersome quickly
  • Object cache uses LRU to evict entries

no no's ☝️

  • Caching paginated archives
  • Caching a query, or other information that is already in the database might not make sense for you
  • Caching full query objects
  • Caching menus
  • Never assume the data stored in a transient is actually there.

Good Ideas 💡

  • Invalidate transient data only when needed
  • Let objects manage their own cache
  • Try to reuse transients whenever possible, think of data, not feature
  • Use object meta as storage
  • Cache external API calls - calculate rate limits

Garbage Collection

  • ... There is none.
  • Renaming a transient will orphan it
  • Orphaned transients in an object cache isn't a big deal
  • `wp transient delete --expired` is your friend
  • Transients that don't have an expiration will be autoloaded from the database
  • Becomes an issue with large orphaned transients

Transient Helpers

DFM-Transients to the rescue!

http://dlopez1986.blogspot.com/2012/09/trying-to-make-animated-gifs.html

About the plugin

  • Built out of frustration with the shortcomings of the transients API.
  • A good solution for high traffic sites utilizing the transients API, but looking for a larger feature set.
  • Supports multiple caching engines.
  • Asynchronous data processing a main focus
  • Full WP-CLI toolset included
  • Find it here -> https://github.com/dfmedia/DFM-Transients

Basic Example

// Register the transient
function register_sample_transient() {

  $transient_args = array(
    'cache_type' => 'transient',
    'callback' => 'transient_callback',
    'expiration' => DAY_IN_SECONDS,
    'soft_expiration' => true,
    'async_updates' => true,
    'update_hooks' => array(
        'updated_post_meta' => 'transient_meta_update_cb',
    ),
  );

   dfm_register_transient( 'sample_transient', $transient_args );

}

add_action( 'after_setup_theme', 'register_sample_transient' );

// Callback to populate the data to store in the transient
function transient_callback( $modifier ) {
  $args = array(
    'post_type' => 'post',
    'post_status' => 'publish',
    'posts_per_page' => 5,
    'tax_query' => array(
      array(
        'taxonomy' => 'category',
        'terms' => $modifier,
      ),
    ),
  );
  $posts = new WP_Query( $args );
  return $posts;
}

// Callback to decide if we should actually regenerate the data on this hook
function transient_meta_update_cb( $args ) {
  // Matches $meta_key value (3rd arg passed to hook)
  if ( 'my_meta_key' === $args[2] ) {
    // Returns post ID
    return $args[1]
  } else {
    // If this callback returns false, we will not regenerate the transient data.
    return false;
}

Wrapping it up

 http://gph.is/1QW9HqB

Takeaways

  • Leveraging transients is a great way to cache the results for complex queries or remote requests, and speed up your site.
  • There's almost no reason not to use transients to cache remote API requests
  • Be aware of the quirks
  • Use an object cache... for real...
  • Never store anything in a transient that you can't recreate on demand
  • Never assume your transient data is actually there

Sources

Questions?

WordPress Transients: A Technical Guide to a Powerful API

By Ryan Kanner

WordPress Transients: A Technical Guide to a Powerful API

  • 2,972