Return to Posts

Speeding Up WordPress REST API Response Times with Cached Data

Developing for the WordPress REST API opens up tons of possibilities for how we communicate with the database from the front-end of websites and apps. Whether you are using a custom endpoint created by register_rest_route, or just adding custom data to the output of an existing endpoint through register_rest_field, it often makes sense to cache some of this data.

Here are a few approaches I’ve taken on production sites that have helped speed up response times for me.

Use the Transient API, Carefully

WordPress transients can be used to store basically anything. Since they’re stored in the wp_options table however, this approach should be used sparingly, and usually with data that is somewhat global in nature. To illustrate — if you had 10,000 entries of a post type and used a transient to store data for each entry, that would lead to the options table becoming 10,000 rows longer (not good). We’ll handle this particular use-case later in this post by using the post meta API.

For now, let’s set a transient that contains a 3rd party API response, and then add it to our own custom endpoint response.

<?php
function yournamespace_rest_transient_example() {
//check for transient first
$data = get_transient( 'yournamespace_data_example' );
if ( !empty( $data ) ) {
$data = $terms_transient;
//generate output and then save to transient
} else {
//do some stuff here and set result to $data
$data = custom_remote_post_call();
//cache for 2 days
set_transient( 'yournamespace_data_example', $data, DAY_IN_SECONDS * 2 );
}
$result = new WP_REST_Response( $data, 200 );
return $result;
}

Now instead of calling the 3rd party API directly in your endpoint, just call the yournamespace_rest_transient_example function to check for the cached response first. If the transient doesn’t exist, it will call the API and store the response.

You can test the actual performance boost this gives you by A/B testing the response time of your endpoint in Postman with the first get_transient() call commented out. Your mileage may vary depending on your server configuration and how expensive the queries you are making are.

Clearing the transient

It often makes sense to delete your custom post meta data whenever a post is saved, although you could choose to just wait until it expires.

<?php
function yournamespace_delete_transient( $post_id, $post, $update ) {
delete_transient('yournamespace_data_example');
}
//{your_post_type} with a custom post type
add_action('save_post_{your_post_type}', 'yournamespace_delete_transient', 10, 3);

Use Post Meta for Expensive Queries

For data that is expensive to generate and is related to a post, it can make sense to store the data in the post’s meta for a defined period of time. We’re essentially recreating the idea of transients but storing them in the wp_postmeta table. First we generate some data and save it to the post’s meta along with an expiration date. Then, in future calls to the endpoint we’ll check if this stored data exists, and return the stored version instead of generating it again from scratch.

This only makes sense if generating the new data takes longer than looking it up from the database. So if your endpoint is handling some complex queries, or using a 3rd party API, this can make a noticeable difference. Here’s how to set things up.

Register your endpoint or custom field and callback function

<?php
/***
Register custom field for existing endpoint with callback function
Endpoint: /wp/v2/your_post_type
Note: Could also be a custom endpoint using register_rest_route
***/
register_rest_field('your_post_type', 'custom_field_name', array(
'get_callback' => 'yournamespace_endpoint_callback',
'schema' => null,
));
/***
Callback function – if you use a custom endpoint you may have to change how you get $post_id
***/
function yournamespace_endpoint_callback( $object ) {
$post_id = $object['id'];
return yournamespace_get_post_transient( $post_id, 'yournamespace_data_example', 'yournamespace_get_custom_meta' );
}

Generate the data and save in post meta with an expiration key

This is where the magic happens. In our helper function called yournamespace_get_post_transient(), we are first checking to see if the data exists in the post meta. If it does and it’s not expired, we can just return it directly. If not, then we generate the data from scratch but store it an array along with an expiration time. Again, this probably only makes sense if the data we are generating is complex.

<?php
/***
Stores persisent data as post meta instead of using Transient API
@param $post_id int post ID to store post meta for
@parm $meta_key str "transient" name
@param $update_func str function to update the meta key with a new value
@param $expiration time when value should expire
return $value array stored value or updated value
***/
function yournamespace_get_post_transient( $post_id, $meta_key, $update_func, $expiration = null ) {
if ( null == $expiration ) {
$expiration = strtotime( '+7 days' );
}
//uncomment and request endpoint to clear cache:
//delete_post_meta($post_id, $meta_key);
$current_value = get_post_meta( $post_id, $meta_key, true );
//this is the "cached" value
//if the meta value is in the right format and it's not expired, we return it and we are done
if ( is_array( $current_value ) && $current_value['expiration'] > time() ) {
return $current_value['data'];
}
//either expired or didn't exist so let's call our "update" function
$new_data = call_user_func( $update_func, $post_id );
//store output in an array so that we can check expiration later
$value = array(
'expiration' => $expiration,
'data' => $new_data
);
//save this new value to post meta
update_post_meta( $post_id, $meta_key, $new_value );
//return just the data for use in the endpoint
return $value['data'];
}
/***
Update function example – this is the data you're actually using in your endpoint or custom field
@param $post_id int post ID to store post meta for
return $meta array Your value
***/
function yournamespace_get_custom_meta( $post_id ) {
$test_meta_value = get_post_meta('test_meta_value', $post_id);
$some_other_thing = get_post_meta('some_other_thing', $post_id);
$expensive = maybe_a_really_crazy_query($post_id);
$meta = array(
'test_thing' => $test_meta_value,
'some_thing' => $some_other_thing,
'expensive_thing' => $expensive
);
return $meta;
}
/***
Clear yc_collection_meta post_meta transient on post save/update
***/
function yournamespace_delete_meta_transient( $post_id, $post, $update ) {
delete_post_meta( $post_id, 'yc_collection_meta' );
yc_get_post_transient( $post_id, 'yc_collection_meta', 'yc_get_collection_meta' );
}
add_action( 'save_post_{your_post_type}', 'yournamespace_delete_meta_transient', 10, 3 );

In your app, whenever you call your endpoint you can now expect to see a cached version of the response output you defined in the yournamespace_get_custom_meta() function.

Deleting the post meta value

Notice the final function yournamespace_delete_meta_transient which is hooked to save_post. This will ‘bust the cache’ by deleting the custom meta value and then regenerating it when a posted is updated.

Make sure to test

The bottom line is that if the process of looking up the transient or post meta data that you have saved and returning it from the database costs more than it does to just generate the data on the fly – just generate the data on the fly. There is no way around this other than to test your endpoints with various caching methods turned on an off and compare results.

December 24, 2019 WordPress Development