24

I'm looking for a definitive answer here. When object caching is enabled, where do options and transients end up living?

By default, both are stored in the database. But I've heard some references that memcache will store them elsewhere and APC will do something else entirely. Where, exactly, will this data be persisted in both cases?

1

4 Answers 4

37

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.
6
  • nitpick 1: Not all options are loaded on page start, but only those that are marked "autoload=yes" when created. The default for that parameter in add_option is 'yes' and most plugin writers don't bother to understand the difference in using a 'no' there make your statement practically true. Dec 3, 2012 at 5:32
  • Even non-autoloaded options are cached after they are fetched once. They may not be loaded initially, but they do go into the object cache after that. Even options that don't exist are cached! github.com/WordPress/WordPress/blob/master/wp-includes/… I added a note about the autoload option, however. Dec 3, 2012 at 5:37
  • that was nitpick 2 ;) Dec 3, 2012 at 5:42
  • Thank you for the great article, and for the time summarizing all that.
    – prosti
    Dec 15, 2016 at 13:54
  • What a brilliant, useful answer, thank you! Jun 11, 2020 at 8:42
6

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.

7
  • I know the answer is fairly old, but I would add also the excellent Redis.
    – Cranio
    Apr 12, 2016 at 10:32
  • @Cranio, you are right but... redis is basically a variant of memcache with storage, and therefor it is a (NoSQL) DB. This IMHO is actually bad as if the node fails or can not be updated you might get stale info from it. It has a option to turn off the DB like behavior but I am not sure if it is on or off by default. Apr 12, 2016 at 10:42
  • It is a perfect replacement for Memcached (even better), what else do you need? By far the most common usage I have seen is just as a RAM key-value storage (yes, other than that, data can be made persistent, clustering is on the way, and has queue management capabilities, but everyone adds Redis as an excellent caching option for WP)
    – Cranio
    Apr 12, 2016 at 12:45
  • everyone can also jump of the bridge ;) but the added complexity is totally not needed for caching Apr 12, 2016 at 13:04
  • That's completely pointless; you want RAM caching, Redis does RAM caching, period; and it does it wonderfully. There absolutely no added complexity if you don't want to go for it. So, sir, I really fail to understand your point.
    – Cranio
    Apr 13, 2016 at 10:41
1

Great question.

I think the part with how WordPress uses WP_Object_Cache class is still missing, so I will add that.

From the docs:

DEF: The WordPress Object Cache is used to save on trips to the database. The Object Cache stores all of the cache data to memory and makes the cache contents available by using a key, which is used to name and later retrieve the cache contents.

Here is the WP_Object_Cache structure.

enter image description here

Note + is public, - private, # protected.

You use the stats() method to show general statistics about the global cache object and what is in there. Here is the output:

Cache Hits: 110
Cache Misses: 98

Group: options - ( 81.03k )
Group: default - ( 0.03k )
Group: users - ( 0.41k )
Group: userlogins - ( 0.03k )
Group: useremail - ( 0.04k )
Group: userslugs - ( 0.03k )
Group: user_meta - ( 3.92k )
Group: posts - ( 1.99k )
Group: terms - ( 1.76k )
Group: post_tag_relationships - ( 0.04k )
Group: category_relationships - ( 0.03k )
Group: post_format_relationships - ( 0.02k )
Group: post_meta - ( 0.36k )

This is what I got before at the very beginning of a template such as single.php.

Note the variable we are interested is: global $wp_object_cache.

The private the member $cache holds the actual caching data.

In programming, cache structures are everywhere. In a simple form they can be recognized as a key value pair. Buckets, NoDB structures, database indexes. WordPress Object Cache ultimate goal was not to have the simplest possible structure, but still key value pairs can be recognized.

Since I was in single.php when I printed the cache:

print_r($wp_object_cache->cache['posts']);

I am getting a single post cached.

    [last_changed] => 0.34169600 1481802075
    [get_page_by_path:2516f01e446b6c125493ec7824b63868:0.34169600 1481802075] => 0
    [2831] => WP_Post Object
        (
            [ID] => 2831
            [post_author] => 1 
            ... the cached post object goes here
        )

The object would be the value, and the caching key would be

get_page_by_path:2516f01e446b6c125493ec7824b63868:0.34169600 1481802075

In here you may check the $cache_key structure:

File: /wp-includes/post.php
4210: /**
4211:  * Retrieves a page given its path.
4212:  *
4213:  * @since 2.1.0
4214:  *
4215:  * @global wpdb $wpdb WordPress database abstraction object.
4216:  *
4217:  * @param string       $page_path Page path.
4218:  * @param string       $output    Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which correspond to
4219:  *                                a WP_Post object, an associative array, or a numeric array, respectively. Default OBJECT.
4220:  * @param string|array $post_type Optional. Post type or array of post types. Default 'page'.
4221:  * @return WP_Post|array|null WP_Post (or array) on success, or null on failure.
4222:  */
4223: function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) {
4224:   global $wpdb;
4225: 
4226:   $last_changed = wp_cache_get_last_changed( 'posts' );
4227: 
4228:   $hash = md5( $page_path . serialize( $post_type ) );
4229:   $cache_key = "get_page_by_path:$hash:$last_changed";
4230:   $cached = wp_cache_get( $cache_key, 'posts' );
4231:   if ( false !== $cached ) {
4232:       // Special case: '0' is a bad `$page_path`.
4233:       if ( '0' === $cached || 0 === $cached ) {
4234:           return;
4235:       } else {
4236:           return get_post( $cached, $output );
4237:       }
4238:   }
0

Options are always stored in the database, while transients may be stored only in shared memory if APC and a plugin that implements APC caching in WP is installed. Memcache uses memory as well.

Options are also stored in memory, and loaded from there when possible (if not, a db query is performed).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.