Wordpress - How does object caching work?

WordPress, by default, does a form of "Object Caching" but its lifetime is only a single page load.

Options are actually a really good example of this. Check out this answer for more info. The summary:

  1. A page starts
  2. All options are loaded with a simple SELECT option_name, option_value from $wpdb->options statement
  3. Subsequent requests for those options (eg a call to get_option never hit the database because they are stored with the WP cache API.

Options always "live" in the database and are always persisted there -- that's their "canonical" source. That said, options are loaded into the object cache so when you request an option there's a 99% chance that request will never hit the database.

Transients are a bit different.

WordPress allows you to replace the cache api with a drop-in -- a file that gets placed directly in your wp-content folder. If you create your own cache drop in or use an existing plugin, you can make the object cache persist longer than a single page load. When you do that, transients, change a bit.

Let's take a look at the set_transient function in wp-includes/option.php.

<?php
/**
 * Set/update the value of a transient.
 *
 * You do not need to serialize values. If the value needs to be serialized, then
 * it will be serialized before it is set.
 *
 * @since 2.8.0
 * @package WordPress
 * @subpackage Transient
 *
 * @uses apply_filters() Calls 'pre_set_transient_$transient' hook to allow overwriting the
 *  transient value to be stored.
 * @uses do_action() Calls 'set_transient_$transient' and 'setted_transient' hooks on success.
 *
 * @param string $transient Transient name. Expected to not be SQL-escaped.
 * @param mixed $value Transient value. Expected to not be SQL-escaped.
 * @param int $expiration Time until expiration in seconds, default 0
 * @return bool False if value was not set and true if value was set.
 */
function set_transient( $transient, $value, $expiration = 0 ) {
    global $_wp_using_ext_object_cache;

    $value = apply_filters( 'pre_set_transient_' . $transient, $value );

    if ( $_wp_using_ext_object_cache ) {
        $result = wp_cache_set( $transient, $value, 'transient', $expiration );
    } else {
        $transient_timeout = '_transient_timeout_' . $transient;
        $transient = '_transient_' . $transient;
        if ( false === get_option( $transient ) ) {
            $autoload = 'yes';
            if ( $expiration ) {
                $autoload = 'no';
                add_option( $transient_timeout, time() + $expiration, '', 'no' );
            }
            $result = add_option( $transient, $value, '', $autoload );
        } else {
            if ( $expiration )
                update_option( $transient_timeout, time() + $expiration );
            $result = update_option( $transient, $value );
        }
    }
    if ( $result ) {
        do_action( 'set_transient_' . $transient );
        do_action( 'setted_transient', $transient );
    }
    return $result;
}

Hmmm $_wp_using_ext_object_cache? If it's true, WordPress uses the object cache instead of the database to store transients. So how does that get set to true? Time to explore how WP sets up its own cache API.

You can trace almost everything to wp-load.php or wp-settings.php -- both of which are crucial to the bootstrap process of WordPress. In our cache, there are some relevant lines in wp-settings.php.

// Start the WordPress object cache, or an external object cache if the drop-in is present.
wp_start_object_cache();

Remember that drop in thing from above? Let's take a look at wp_start_object_cache in wp-includes/load.php.

<?php
/**
 * Starts the WordPress object cache.
 *
 * If an object-cache.php file exists in the wp-content directory,
 * it uses that drop-in as an external object cache.
 *
 * @access private
 * @since 3.0.0
 */
function wp_start_object_cache() {
    global $_wp_using_ext_object_cache, $blog_id;

    $first_init = false;
    if ( ! function_exists( 'wp_cache_init' ) ) {
        if ( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
            require_once ( WP_CONTENT_DIR . '/object-cache.php' );
            $_wp_using_ext_object_cache = true;
        } else {
            require_once ( ABSPATH . WPINC . '/cache.php' );
            $_wp_using_ext_object_cache = false;
        }
        $first_init = true;
    } else if ( !$_wp_using_ext_object_cache && file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
        // Sometimes advanced-cache.php can load object-cache.php before it is loaded here.
        // This breaks the function_exists check above and can result in $_wp_using_ext_object_cache
        // being set incorrectly. Double check if an external cache exists.
        $_wp_using_ext_object_cache = true;
    }

    // If cache supports reset, reset instead of init if already initialized.
    // Reset signals to the cache that global IDs have changed and it may need to update keys
    // and cleanup caches.
    if ( ! $first_init && function_exists( 'wp_cache_switch_to_blog' ) )
        wp_cache_switch_to_blog( $blog_id );
    else
        wp_cache_init();

    if ( function_exists( 'wp_cache_add_global_groups' ) ) {
        wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'site-transient', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache' ) );
        wp_cache_add_non_persistent_groups( array( 'comment', 'counts', 'plugins' ) );
    }
}

The relevant lines of the function (the ones that pertain to $_wp_using_ext_object_cache that alters how transients are stored).

if ( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
    require_once ( WP_CONTENT_DIR . '/object-cache.php' );
    $_wp_using_ext_object_cache = true;
} else {
    require_once ( ABSPATH . WPINC . '/cache.php' );
    $_wp_using_ext_object_cache = false;
}

if object-cache.php exist in your content directory it gets included and WP assumes you're using an external, persistent cache -- it sets $_wp_using_ext_object_cache to true.

If you're using an external object cache transients will use it. Which brings up the question of when to use options vs. transients.

Simple. If you need data to persist indefinitely, use options. They get "cached", but their canonical sources is the database and they will never go away unless a user explicitly requests it.

For data that should be stored for a set amount of time, but does not need to persist beyond a specified lifetime use transients. Internally, WP will try to use an external, persistent object cache if it can otherwise data will go into the options table and get garbage collected via WordPress' psuedo-cron when they expire.

Some other concerns/questions:

  1. Is it okay to do a ton of calls to get_option? Probably. They incur the call to a function overhead, but it likely won't hit the database. Database load is often a bigger concern in web application scalability than the work your language of choice does generating a page.
  2. How do I know to use transients vs. the Cache API? If you expect data to persist for a set period, use the transient API. If it doesn't matter if data persists (eg. it doesn't take long to compute/fetch the data, but it shouldn't happen more than once per page load) use the cache API.
  3. Are all options really cached on every pageload? Not necessarily. If you call add_option with its last, optional argument as no they are not autoloaded. That said, once you fetch them once, they go into the cache and subsequent calls won't hit the database.

There are 4 cache types that I know of

  1. Trivial - It is always on and takes affect before any other caching comes into play. It stores the cached items in an php array which means that it consumes memory from your php execution session, and that the cache is emptied after php execution is over. i.e. even without using any other cache if you call get_option('opt') twice in a row you will make a DB query only the first time and the second time the value will be returned from memory.

  2. File - Cached values are stored in files somewhere under your root directory. I believe it proved to be not effective in terms of performance unless you have a very fast disk or memory mapped file storage.

  3. APC (or other php accelerator based caching) - Cached values are stored in the memory of your host machine and outside of your php memory allocation. The biggest potential pitfall is that there is no scoping of data and if you run two sites potentially each can access the cached data of the other, or overwrite it.

  4. Memcache - it is a network based cache. You can run the caching service anywhere on the network and It probably stores values in its host memory. You probably don't need memcache unless you have a load balancing in action.

BTW, object caching is caching much more than options, it will store almost anything that was retrieved from the DB using high level WP API.