WordPress REST API versus Custom Request Handlers

Last year I wrote a blog post comparing the performance of using admin-ajax.php and the WordPress REST API, and found that the REST API was about 16% faster than using the traditional AJAX API. While that was a solid improvement, quite a few developers (myself included) were wondering how the REST API compares to using completely custom endpoints. Since the REST API loads most of WordPress core and any active plugins, it should be quite a difference!

There’s little doubt that a custom endpoint will be faster, but it should be interesting to see how much faster it will be. It’s also worth taking a look to see if a custom endpoint makes sense from a development and future support perspective.

Defining Custom Request Handlers

There are a few different ways to set up custom endpoints. The fastest possible endpoint would likely be completely independent of WordPress, and could be as simple as a standalone PHP file that is uploaded to the web server. While fast, it probably wouldn’t be all that helpful, because we won’t have access to any WordPress core functions which often save a great deal of development time.

Speeding Things Up with SHORTINIT

The next fastest approach would be to manually load enough of WordPress to be able to use core functions while not loading things like themes and plugins. AJAX requests would target this file specifically, so it should be placed somewhere on the server where it is publicly accessible.

There’s a few ways of doing this, but this is the most common approach I see:

<?php
// custom-ajax-endpoint.php

define( 'DOING_AJAX', true );

// Tell WordPress to load as little as possible
define( 'SHORTINIT', true );

require_once '../../wp-load.php';

wp_send_json( array( 'time' => time() ) );

The above file does a few things. First it defines the DOING_AJAX constant to tell WordPress that an AJAX request is being made. Then it defines the SHORTINIT constant to tell WordPress to load the basics – some of WordPress core and not much else. Then it manually requires the wp-load.php file (which is generally bad practice as the location of this file can vary between installs) which loads some other files needed to properly serve the request. Finally it responds to the request with the JSON encoded time.

While looking at this, my colleague Evan noticed another approach that doesn’t require knowing the location of the wp-load.php file. This can be accomplished by listening for a parameter that defines a custom request in the wp-config.php file:

/**
 * Define SHORTINIT and DBI_AJAX constants if this is 
 * a request to our custom request handler
 */
if ( filter_input( INPUT_GET, 'dbi-ajax' ) ) {
    define( 'SHORINIT', true );
    define( 'DBI_AJAX', true );
}

/* That's all, stop editing! Happy blogging. */

/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') )
    define('ABSPATH', dirname(__FILE__) . '/');

/** Sets up WordPress vars and included files. */
require_once(ABSPATH . 'wp-settings.php');

if ( defined( 'DBI_AJAX' ) ) {
    wp_send_json( array( 'time' => time() ) );
}

This is similar to the first approach above. If the request contains a dbi-ajax parameter (so we know that this is a custom request), we define the SHORTINIT constant to tell WordPress to load the bare essentials. We also define a DBI_AJAX constant so we can easily tell if a custom request is being made without checking for the dbi-ajax parameter again. Then once WordPress has been loaded (after the wp-settings.php file has been required), we can serve the request. Usually we would recommend including another file at this point which contains that functionality, but for the purpose of this benchmark we’re just returning the JSON encoded time like in the first example above.

Loading so little of WordPress comes with a unique set of challenges. Not all WordPress core functions will be available, so if a function that you need isn’t available, you’ll need to track down the file it resides in and include it manually. Depending on what your request handler needs to do, this could become a burden. Also, manually requiring the wp-load.php file or editing the wp-config.php file isn’t portable and likely shouldn’t be done in a plugin or theme that will be distributed across a large number of installs.

A Better Alternative?

Another possible approach is to create a custom request handler within a Must Use plugin. That way it can respond to any custom requests after WordPress core loads, but before the plugins and themes are loaded. This should both improve performance and prevent conflicts from other plugins that are using the REST API or admin-ajax.php.

<?php
/**
 * Plugin Name: DBI AJAX Endpoint
 * Description: Custom Request endpoint
 * Author: Delicious Brains Inc
 * Version: 1.0
 * Author URI: https://deliciousbrains.com
 */

if ( ! filter_input( INPUT_GET, 'dbi-ajax' ) ) {
    return;
}

// Define the WordPress "DOING_AJAX" constant.
if ( ! defined( 'DOING_AJAX' ) ) {
    define( 'DOING_AJAX', true );
}


wp_send_json( array( 'time' => time() ) );

The above plugin listens for requests that contain the dbi-ajax parameter (to prevent it from responding to any request), and then uses wp_send_json() to return the time in JSON to complete the request like in the previous examples.

Running Some Benchmarks

With two potential solutions for a custom request endpoint in place, it’s time to run some benchmarks. To keep things somewhat consistent with the previous article, I’m using ApacheBench to send hundreds of HTTP requests to the server in a short time in order to get a feel for the average response time of the endpoint. I also have the same set of plugins activated:

  • ACF
  • Akismet
  • Black Studio TinyMCE Widget
  • WP Migrate DB
  • WP Super Cache
  • Yoast SEO

First let’s take another look at the REST API benchmark so we have a solid baseline:

REST API benchmark

After 100 requests, an average response time of 264ms becomes apparent. This is actually much faster than it was last year when I ran the same test on the same server (490ms), but for now let’s just focus on the 264ms average response time as a baseline with which we can judge the results of the custom endpoints.

SHORTINIT Benchmark

Next let’s take a look at the custom endpoint that loads the bare minimum files by defining the SHORTINIT constant.

Standalone custom endpoint benchmark

With an average response time of just 29ms (an 89% decrease), it’s clear that this approach is night-and-day faster than using the REST API. This is because only the files needed to serve the request are loaded.

Must Use Plugin Benchmark

Finally let’s take a look at the custom endpoint that was created in the mu-plugin:

Must Use plugin custom endpoint benchmark

With an average response time of 144ms (a 45% decrease compared to the REST API), the performance of this endpoint is somewhere in between the standalone file and the REST API. This makes it a good solution for a custom request handler that can use WordPress core functions without knowing the location of the wp-load.php file.

Conclusions

Response times shouldn’t be your only consideration when developing an application that will send a large amount of requests. It would also be wise to consider the upfront development time as well as any time that you might spend supporting the endpoints.

For some use-cases where performance is critical, such as plugins that process a lot of data in batches or where the potential for plugin/theme conflicts is higher, it might make sense to use a custom endpoint. Most of the time, having a pre-made, officially supported solution will far outweigh the potential performance benefits of creating your own endpoints.

Are you using custom endpoints in your plugin or theme? If so, would you consider making the switch to the REST API? Why or why not? Let me know in the comments.

About the Author

Matt Shaw

Matt is a WordPress plugin developer located near Philadelphia, PA. He loves to create awesome new tools with PHP, Javascript, and whatever else he happens to get his hands on.

  • Phil

    We use the REST API Extensively, but recently have started using a Node.js API middleware to cache and obfuscate the headless wordpress ‘mothership’ API – plus we are using the content in mobile apps, so having the faster Node layer makes it much ‘better’ for growth.

  • I’d be curious to see benchmarking for WPGraphQL (https://github.com/wp-graphql/wp-graphql) queries compared to REST as well. . .in my non-scientific tests, it’s much faster than REST (mostly because with GraphQL queries you can get just the data you need, not entire payloads of data that you might not want or care for). . .it should also get even faster when this DataLoader gets implemented (https://github.com/wp-graphql/wp-graphql/issues/55)

  • Mike Lee

    Great article. I’ve long been drawn to the new REST API but the response times were too slow. For some of the enterprise projects our company works on, I developed a
    custom endpoint to use and since then we’ve had response times 10-15X
    faster than the standard admin-ajax.php endpoint. One example is an AJAX request I wrote to extend a user’s session (our users have 15 min sessions, but so long as they’re clicking around and showing activity we extend the session on each click). Admin-ajax.php was generally 700-1000ms roundtrip.

    Our custom solution is generally 55-80ms in the Production application. We use SHORTINT in a file that sits in the docroot, achieved by pulling in only some of WP’s core (which also means we have to hand-tune which files are loaded from the Core so that things like WP_Query work). It’s added development time to maintain it, of course, but it also
    allows us to tune the returned JSON to be much more specific (sort of
    like what you’d get from GraphQL in some ways). And since our application uses a ton of ajax, loading up only parts of WP’s core on each request has helped cpu/memory stay on the lower side as things have scaled.

  • Another good read, Matt. I did a similar comparison for a custom situation I was in and decided in favour of building a custom API myself. Though, it was a refreshing experiment.

  • Interesting stuff. Was this with or without an opcode cache (e.g. opcache) and a persistent object cache (e.g. memcached, redis)?

    • Matt Shaw

      Thanks! This was done without any caching, although it might be cool to run similar benchmarks with different types of caching in a follow up.

      • I reckon so. An opcode cache should reduce the impact of those huge plugins on the results, even in PHP 7. Quite likely memcached / redis would thin the difference a little more, albeit not as much as an opcode cache.

  • guy

    Great information, interesting insight as always. It would be great if you guys could write a full and unbiased post about the security of the WP Rest API. This is one area of the WP REST API that seems to put a lot of people off embracing it, especially while the “big name” security plugin developers flag it as a “threat”..

  • schwarz45234

    Very interesting topics you share with us and i hope most of the children are enjoy this edition. They find more information from here about their stomach in here. I like this post so more.

  • This is fantastic for performance, but it seems like rest_api_init is ignored in the custom file (using the SHORTINIT method). Are you then responsible for writing all routing logic? Or are there other files to include to use register_rest_route to actually utilize the WordPress REST methods?

    For example – I have a .htaccess rule routing all requests for “wp-json” (or “api” tried fully custom on that) to my custom endpoint.php file, but rest_api_init is ignored and the callback doesn’t fire. I’ll keep up the trial and error, but any tips to save me a headache or 10?

  • poplin42346

    Very interesting topics you share with us and i hope most of the children are enjoy this edition. They find more information from here about their stomach in here. I like this post so more.